Skill: tristanmanchester/fabric-api (version 1.0.1)

--- name: fabric-api description: Create, search, and manage Fabric resources via the Fabric HTTP API (notepads/notes, folders, bookmarks, files, tags). homepage: https://fabric.so metadata: {"openclaw":{"emoji":"🧵","homepage":"https://fabric.so","requires":{"env":["FABRIC_API_KEY"],"anyBins":["node","python3","python","curl"]},"primaryEnv":"FABRIC_API_KEY"},"clawdbot":{"emoji":"🧵","homepage":"https://fabric.so","requires":{"env":["FABRIC_API_KEY"],"anyBins":["node","python3","python","curl"]},"primaryEnv":"FABRIC_API_KEY"}} --- # Fabric API (HTTP via Node/Python) Use this skill when you need to **read or write content in a user's Fabric workspace** using the Fabric HTTP API (`https://api.fabric.so`). This version avoids bash-only wrapper scripts. It ships **cross-platform** helpers: - Node: `{baseDir}/scripts/fabric.mjs` (recommended) - Python: `{baseDir}/scripts/fabric.py` ## Critical gotchas (read first) - **There is no** `POST /v2/notes` endpoint in the bundled OpenAPI spec. To create a “note”, use **`POST /v2/notepads`**. - Most create endpoints require **`parentId`**: - A folder UUID **or** one of: `@alias::inbox`, `@alias::bin` - Notepad creation requires: - `parentId` - and **either** `text` (markdown string) **or** `ydoc` (advanced/structured) - `tags` must be **an array of objects**, each item **either**: - `{ "name": "tag name" }` **or** `{ "id": "<uuid>" }` - Never strings, never nested arrays. - **Field name gotcha:** the API schema uses `name` (not `title`). If the user says “title”, map it to `name` in requests. When the user doesn’t specify a destination folder, default to: - `parentId: "@alias::inbox"` ## Setup (OpenClaw / Clawdbot) This skill expects the Fabric API key in: - `FABRIC_API_KEY` OpenClaw config example (`~/.openclaw/openclaw.json`): ```json5 { skills: { entries: { "fabric-api": { enabled: true, [REDACTED_SECRET]" } } } } ``` Notes: - `apiKey` is a convenience for skills that declare `primaryEnv`; it injects `FABRIC_API_KEY` for the duration of an agent run. - Don’t paste the API key into prompts, client-side code, or logs. ## HTTP basics - Base URL: `https://api.fabric.so` (override with `FABRIC_BASE` if needed) - Auth header: `X-[REDACTED_SECRET]` - JSON header (for JSON bodies): `Content-Type: application/json` ## Convenience scripts (cross-platform) ### Node helper (recommended) ```bash node {baseDir}/scripts/fabric.mjs GET /v2/user/me node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"name":"Test note","text":"Hello","parentId":"@alias::inbox"}' ``` ### Python helper ```bash python3 {baseDir}/scripts/fabric.py GET /v2/user/me python3 {baseDir}/scripts/fabric.py POST /v2/notepads --json '{"name":"Test note","text":"Hello","parentId":"@alias::inbox"}' ``` Notes: - Both helpers print the response body on success. - On HTTP errors (4xx/5xx), they print `HTTP <code> <reason>` to stderr **and still print the response body**, then exit non‑zero (similar to `curl --fail-with-body`). - If you pass an absolute URL (`https://...`), the helpers **do not** attach `X-Api-Key` unless you explicitly pass `--with-key`. ## Core workflows ### 1) Create a notepad (note) Endpoint: `POST /v2/notepads` Rules: - Map user “title” → `name` - Use `text` for markdown content - Always include `parentId` - If you’re debugging 400s, start minimal (required fields only), then add `name`, then `tags`. Minimal create: ```bash node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"parentId":"@alias::inbox","text":"Hello"}' ``` Create with a name: ```bash node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"name":"Calendar Test Note","text":"Created via OpenClaw","parentId":"@alias::inbox"}' ``` Create with tags (correct shape): ```bash node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"name":"Ideas","text":"# Ideas\\n\\n- First\\n- Second\\n","parentId":"@alias::inbox","tags":[{"name":"ideas"},{"name":"draft"}]}' ``` If you keep seeing tag validation errors, temporarily omit `tags` and create the notepad first. ### 2) Create a folder Endpoint: `POST /v2/folders` ```bash node {baseDir}/scripts/fabric.mjs POST /v2/folders --json '{"name":"My new folder","parentId":"@alias::inbox","description":null}' ``` ### 3) Create a bookmark Endpoint: `POST /v2/bookmarks` ```bash node {baseDir}/scripts/fabric.mjs POST /v2/bookmarks --json '{"url":"[REDACTED_SECRET]","name":"Example","tags":[{"name":"reading"}]}' ``` ### 4) Browse resources (list children of a folder) Endpoint: `POST /v2/resources/filter` Important: - This endpoint’s `parentId` expects a **UUID** (not an alias). - If you only have an alias, resolve it by listing resource roots and picking the inbox/bin folder ID. ```bash node {baseDir}/scripts/fabric.mjs POST /v2/resources/filter --json '{"parentId":"PARENT_UUID_HERE","limit":50,"order":{"property":"modifiedAt","direction":"DESC"}}' ``` ### 5) Search Endpoint: `POST /v2/search` Use search when the user gives a fuzzy description (“the note about…”). ```bash node {baseDir}/scripts/fabric.mjs POST /v2/search --json '{"queries":[{"mode":"text","text":"meeting notes","filters":{"kinds":["notepad"]}}],"pagination":{"page":1,"pageSize":20},"sort":{"field":"modifiedAt","order":"desc"}}' ``` ## Error handling + retries (practical guidance) - **400 Bad Request**: schema validation. Re-check required fields, and that `tags` is `[{name}|{id}]` not nested. - **401/403**: auth/subscription/permission. Stop and report the error details; don’t brute-force. - **404**: wrong endpoint, wrong ID, or no access. - **429**: rate limiting. Back off (sleep + jitter) and retry **reads**. Avoid blind retries on **create** (you may create duplicates). - **5xx**: transient; retry with backoff. ## Reference files - OpenAPI spec (source of truth): `{baseDir}/fabric-api.yaml` - Extra schema notes: `{baseDir}/references/REFERENCE.md` - Debug playbook: `{baseDir}/references/TROUBLESHOOTING.md`

path: fabric-api.yaml
content: openapi: 3.0.3
info:
  version: 1.0.0
  title: Fabric API
servers:
  - url: https://api.fabric.so
    description: Fabric API
security:
  - OAuth2: []
  - ApiKey: []
paths:
  /v2/bookmarks:
    post:
      tags:
        - Bookmarks
      summary: Create bookmark
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                tags:
                  type: array
                  items:
                    anyOf:
                      - type: object
                        properties:
                          id:
                            type: string
                            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                        required:
                          - id
                      - type: object
                        properties:
                          name:
                            type: string
                            maxLength: 255
                        required:
                          - name
                parentId:
                  anyOf:
                    - type: string
                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                    - $ref: '#/components/schemas/RootAlias'
                  description: Parent resource ID or an alias (the alias is resolved to the actual ID during the request handling).
                comment:
                  allOf:
                    - $ref: '#/components/schemas/CommentCreate'
                    - nullable: true
                name:
                  type: string
                  nullable: true
                  minLength: 1
              required:
                - url
                - parentId
      responses:
        '201':
          description: Bookmark created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookmarkCreationResponse'
  /v2/resources/{resourceId}/comments:
    get:
      tags:
        - Comments
      summary: List comments
      parameters:
        - schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
          required: true
          name: resourceId
          in: path
        - schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
          required: false
          name: limit
          in: query
        - schema:
            type: integer
            nullable: true
            minimum: 0
          required: false
          name: offset
          in: query
        - schema:
            type: string
          required: false
          name: accessToken
          in: query
      responses:
        '200':
          description: List of comments
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: number
                    minimum: 0
                  data:
                    type: object
                    properties:
                      comments:
                        type: array
                        items:
                          $ref: '#/components/schemas/CommentWithAuthor'
                    required:
                      - comments
                required:
                  - count
                  - data
        '404':
          description: Resource not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                    enum:
                      - Not Found
                  detail:
                    type: string
                    enum:
                      - resource_not_found
                required:
                  - title
                  - detail
  /v2/files:
    post:
      tags:
        - Files
      summary: Create file
      description: To create a file item you first need to store it using the upload endpoint.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FileCreation'
      responses:
        '201':
          description: File created
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ResourceDetailDocument'
                  - $ref: '#/components/schemas/ResourceDetailAudio'
                  - $ref: '#/components/schemas/ResourceDetailVideo'
                  - $ref: '#/components/schemas/ResourceDetailImage'
                  - $ref: '#/components/schemas/ResourceDetailDefault'
  /v2/folders:
    post:
      tags:
        - Folders
      summary: Create folder
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  nullable: true
                  minLength: 1
                  maxLength: 255
                parentId:
                  anyOf:
                    - type: string
                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                    - $ref: '#/components/schemas/RootAlias'
                  description: Parent resource ID or an alias (the alias is resolved to the actual ID during the request handling).
                description:
                  type: string
                  nullable: true
                icon:
                  oneOf:
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - emoji
                        emoji:
                          type: string
                          minLength: 1
                      required:
                        - type
                        - emoji
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - predefined
                        predefined:
                          type: string
                          minLength: 1
                      required:
                        - type
                        - predefined
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - customImage
                        customImage:
                          type: string
                          minLength: 1
                      required:
                        - type
                        - customImage
                    - nullable: true
              required:
                - parentId
      responses:
        '201':
          description: Folder created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceDetailFolder'
        '404':
          description: Resource not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                    enum:
                      - Not Found
                  detail:
                    type: string
                    enum:
                      - resource_not_found
                required:
                  - title
                  - detail
  /v2/notepads:
    post:
      tags:
        - Notepads
      summary: Create notepad
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - type: object
                  properties:
                    name:
                      type: string
                      nullable: true
                      minLength: 1
                      maxLength: 255
                    parentId:
                      anyOf:
                        - type: string
                          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                        - $ref: '#/components/schemas/RootAlias'
                      description: Parent resource ID or an alias (the alias is resolved to the actual ID during the request handling).
                    tags:
                      type: array
                      items:
                        anyOf:
                          - type: object
                            properties:
                              id:
                                type: string
                                pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                            required:
                              - id
                          - type: object
                            properties:
                              name:
                                type: string
                                maxLength: 255
                            required:
                              - name
                    comment:
                      allOf:
                        - $ref: '#/components/schemas/CommentCreate'
                        - nullable: true
                  required:
                    - parentId
                - anyOf:
                    - type: object
                      properties:
                        text:
                          type: string
                      required:
                        - text
                    - type: object
                      required:
                        - ydoc
      responses:
        '201':
          description: Notepad created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceDetailNotepad'
  /v2/resources/{resourceId}:
    get:
      tags:
        - Resources
      summary: Get resource
      parameters:
        - schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
          required: true
          name: resourceId
          in: path
      responses:
        '200':
          description: Resource detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceDetail'
  /v2/resources/filter:
    post:
      tags:
        - Resources
      summary: List resources
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                kind:
                  type: array
                  items:
                    $ref: '#/components/schemas/Kind'
                kindExclude:
                  type: array
                  items:
                    $ref: '#/components/schemas/Kind'
                rootId:
                  type: string
                  pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                tagIds:
                  type: array
                  items:
                    type: string
                    pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                name:
                  type: string
                url:
                  type: string
                  format: uri
                  description: Look up by the URL of a bookmark kind resource.
                parentId:
                  type: string
                  nullable: true
                  pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                parentRoot:
                  type: object
                  properties:
                    type:
                      $ref: '#/components/schemas/RootType'
                    subtype:
                      anyOf:
                        - $ref: '#/components/schemas/IntegrationType'
                        - $ref: '#/components/schemas/SystemSubtypes'
                    isPrivate:
                      type: boolean
                resourceIds:
                  type: array
                  items:
                    type: string
                    pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                label:
                  type: number
                  nullable: true
                  description: Label of the resource - pass null for resources that have no label.
                shared:
                  type: array
                  items:
                    $ref: '#/components/schemas/SharingFilters'
                rootTypes:
                  type: array
                  items:
                    $ref: '#/components/schemas/RootType'
                includeRoots:
                  type: boolean
                  default: false
                skipInclusions:
                  type: boolean
                  default: false
                createdBefore:
                  type: string
                  nullable: true
                createdAfter:
                  type: string
                  nullable: true
                modifiedBefore:
                  type: string
                  nullable: true
                modifiedAfter:
                  type: string
                  nullable: true
                includeDescendants:
                  type: integer
                  maximum: 3
                  minimum: 0
                includeSubfolderCount:
                  type: boolean
                limit:
                  type: integer
                  minimum: 0
                  exclusiveMinimum: true
                  default: 20
                cursor:
                  type: string
                random:
                  type: boolean
                userIds:
                  type: array
                  items:
                    type: string
                    pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                order:
                  type: object
                  properties:
                    property:
                      type: string
                      enum:
                        - createdAt
                        - createdAt
                        - modifiedAt
                        - kind
                        - name
                    direction:
                      type: string
                      enum:
                        - ASC
                        - DESC
                      default: ASC
                  required:
                    - property
      responses:
        '200':
          description: Resource filter results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceFilterResponse'
  /v2/resource/recover:
    post:
      tags:
        - Resources
      summary: Recover resources
      parameters:
        - schema:
            type: string
          required: false
          name: accessToken
          in: query
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                resourceIds:
                  type: array
                  items:
                    type: string
                    pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                  minItems: 1
              required:
                - resourceIds
      responses:
        '204':
          description: Resources recovered
  /v2/resource/delete:
    post:
      tags:
        - Resources
      summary: Delete resources
      parameters:
        - schema:
            type: string
          required: false
          name: accessToken
          in: query
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                resourceIds:
                  type: array
                  items:
                    type: string
                    pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                  minItems: 1
                archive:
                  type: boolean
                  default: false
              required:
                - resourceIds
      responses:
        '204':
          description: Resources deleted
  /v2/resource-roots/{resourceRootId}:
    get:
      tags:
        - Resource root
      summary: Get resource root
      parameters:
        - schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
          required: true
          name: resourceRootId
          in: path
      responses:
        '200':
          description: Root detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceRootDetail'
  /v2/resource-roots:
    get:
      tags:
        - Resource root
      summary: List resource roots
      parameters:
        - schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
          required: false
          name: limit
          in: query
        - schema:
            type: integer
            nullable: true
            minimum: 0
          required: false
          name: offset
          in: query
        - schema:
            type: object
            properties:
              property:
                type: string
                enum:
                  - name
                  - name
              direction:
                type: string
                enum:
                  - ASC
                  - DESC
                default: ASC
            required:
              - property
          required: false
          name: order
          in: query
        - schema:
            type: array
            items:
              type: object
              properties:
                type:
                  $ref: '#/components/schemas/RootType'
                subtype:
                  anyOf:
                    - $ref: '#/components/schemas/IntegrationType'
                    - $ref: '#/components/schemas/SystemSubtypes'
          required: false
          name: types
          in: query
        - schema:
            type: string
          required: false
          name: search
          in: query
        - schema:
            type: array
            items:
              $ref: '#/components/schemas/PermissionRole'
            minItems: 1
          required: false
          name: role
          in: query
        - schema:
            type: number
            nullable: true
            minimum: 0
            maximum: 10
          required: false
          name: includeMembers
          in: query
        - schema:
            type: number
            nullable: true
            minimum: 0
            maximum: 10
          required: false
          name: includeResources
          in: query
      responses:
        '200':
          description: List of roots
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: number
                    minimum: 0
                  data:
                    type: object
                    properties:
                      roots:
                        type: array
                        items:
                          $ref: '#/components/schemas/ResourceRootListItem'
                      integrations:
                        type: array
                        items:
                          allOf:
                            - $ref: '#/components/schemas/PublicIntegrationSchema'
                            - type: object
                              properties:
                                mirrorRegistries:
                                  type: array
                              required:
                                - mirrorRegistries
                      sharedWith:
                        type: object
                        additionalProperties:
                          type: object
                          properties:
                            members:
                              type: array
                              items:
                                allOf:
                                  - $ref: '#/components/schemas/UserProfilePublic'
                                  - type: object
                                    properties:
                                      permission:
                                        type: object
                                        properties:
                                          role:
                                            $ref: '#/components/schemas/PermissionRole'
                                        required:
                                          - role
                                    required:
                                      - permission
                            totalMembers:
                              type: number
                          required:
                            - members
                            - totalMembers
                      resources:
                        type: object
                        additionalProperties:
                          type: array
                          items:
                            $ref: '#/components/schemas/ResourceDetail'
                    required:
                      - roots
                      - integrations
                required:
                  - count
                  - data
  /v2/search:
    post:
      tags:
        - Search
      summary: Search
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - oneOf:
                    - type: object
                      properties:
                        text:
                          type: string
                          maxLength: 255
                      required:
                        - mode
                        - text
                    - type: object
                - type: object
                  properties:
                    filters:
                      type: object
                      properties:
                        kinds:
                          type: array
                          items:
                            type: string
                            enum:
                              - default
                              - image
                              - document
                              - folder
                              - bookmark
                              - highlight
                              - notepad
                              - video
                              - audio
                              - email
                              - person
                              - voicenote
                        color:
                          type: array
                          items:
                            type: number
                        parentIds:
                          type: array
                          items:
                            type: string
                            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                        ancestorIds:
                          type: array
                          items:
                            type: string
                            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                        rootIds:
                          type: array
                          items:
                            type: string
                            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                        roots:
                          type: array
                          items:
                            type: object
                            properties:
                              type:
                                $ref: '#/components/schemas/RootType'
                              subtype:
                                anyOf:
                                  - $ref: '#/components/schemas/SystemSubtypes'
                                  - $ref: '#/components/schemas/IntegrationType'
                              isPrivate:
                                type: boolean
                            required:
                              - type
                        excludeRoots:
                          type: array
                          items:
                            type: object
                            properties:
                              type:
                                $ref: '#/components/schemas/RootType'
                              subtype:
                                anyOf:
                                  - $ref: '#/components/schemas/SystemSubtypes'
                                  - $ref: '#/components/schemas/IntegrationType'
                              isPrivate:
                                type: boolean
                            required:
                              - type
                        tagIds:
                          type: array
                          items:
                            type: string
                            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                        userIds:
                          type: array
                          items:
                            type: string
                            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                        createdAfter:
                          type: string
                          nullable: true
                        createdBefore:
                          type: string
                          nullable: true
                    precision:
                      type: number
                - type: object
                  properties:
                    queries:
                      type: array
                      items:
                        allOf:
                          - oneOf:
                              - type: object
                                properties:
                                  text:
                                    type: string
                                    maxLength: 255
                                required:
                                  - mode
                                  - text
                              - type: object
                          - type: object
                            properties:
                              filters:
                                type: object
                                properties:
                                  kinds:
                                    type: array
                                    items:
                                      type: string
                                      enum:
                                        - default
                                        - image
                                        - document
                                        - folder
                                        - bookmark
                                        - highlight
                                        - notepad
                                        - video
                                        - audio
                                        - email
                                        - person
                                        - voicenote
                                  color:
                                    type: array
                                    items:
                                      type: number
                                  parentIds:
                                    type: array
                                    items:
                                      type: string
                                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                                  ancestorIds:
                                    type: array
                                    items:
                                      type: string
                                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                                  rootIds:
                                    type: array
                                    items:
                                      type: string
                                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                                  roots:
                                    type: array
                                    items:
                                      type: object
                                      properties:
                                        type:
                                          $ref: '#/components/schemas/RootType'
                                        subtype:
                                          anyOf:
                                            - $ref: '#/components/schemas/SystemSubtypes'
                                            - $ref: '#/components/schemas/IntegrationType'
                                        isPrivate:
                                          type: boolean
                                      required:
                                        - type
                                  excludeRoots:
                                    type: array
                                    items:
                                      type: object
                                      properties:
                                        type:
                                          $ref: '#/components/schemas/RootType'
                                        subtype:
                                          anyOf:
                                            - $ref: '#/components/schemas/SystemSubtypes'
                                            - $ref: '#/components/schemas/IntegrationType'
                                        isPrivate:
                                          type: boolean
                                      required:
                                        - type
                                  tagIds:
                                    type: array
                                    items:
                                      type: string
                                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                                  userIds:
                                    type: array
                                    items:
                                      type: string
                                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                                  createdAfter:
                                    type: string
                                    nullable: true
                                  createdBefore:
                                    type: string
                                    nullable: true
                              precision:
                                type: number
                    incognito:
                      type: boolean
                      nullable: true
                      default: false
                      description: Does not store search query in search history, if set to true.
                    pagination:
                      type: object
                      properties:
                        page:
                          type: integer
                          minimum: 0
                          exclusiveMinimum: true
                          default: 1
                        pageSize:
                          type: integer
                          minimum: 0
                          exclusiveMinimum: true
                          default: 20
                    sort:
                      type: object
                      properties:
                        field:
                          type: string
                        order:
                          type: string
                          enum:
                            - asc
                            - desc
                          default: asc
                      required:
                        - field
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: number
                  hasMore:
                    type: boolean
                  hits:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/ResourceDetail'
                        - type: object
                          properties:
                            score:
                              type: number
                            embeddings:
                              type: object
                              properties:
                                small3_512:
                                  type: array
                                  items:
                                    type: number
                                clip2:
                                  type: array
                                  items:
                                    type: number
                          required:
                            - score
                required:
                  - total
                  - hasMore
                  - hits
  /v2/spaces/{spaceId}:
    delete:
      tags:
        - Spaces
      summary: Delete space
      parameters:
        - schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
          required: true
          name: spaceId
          in: path
      responses:
        '204':
          description: Space deleted
        '404':
          description: Space not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                    enum:
                      - Not Found
                  detail:
                    type: string
                    enum:
                      - space_not_found
                required:
                  - title
                  - detail
  /v2/spaces:
    post:
      tags:
        - Spaces
      summary: Create space
      parameters:
        - schema:
            type: string
          required: false
          name: accessToken
          in: query
      requestBody:
        content:
          application/json:
            schema:
              anyOf:
                - type: object
                  properties:
                    name:
                      type: string
                      nullable: true
                    private:
                      type: boolean
                  required:
                    - name
                - type: object
                  properties:
                    moveFolderId:
                      type: string
                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                  required:
                    - moveFolderId
                - type: object
                  properties:
                    copyFolderId:
                      type: string
                      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                    private:
                      type: boolean
                  required:
                    - copyFolderId
      responses:
        '201':
          description: Space created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Space'
  /v2/subscription:
    get:
      tags:
        - Subscription
      summary: Get user subscription
      parameters:
        - schema:
            type: array
            items:
              $ref: '#/components/schemas/UsageType'
            minItems: 1
            description: By default, all usage types are returned
          required: false
          name: type
          in: query
      responses:
        '204':
          description: Subscription information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceSubscription'
  /v2/tags:
    get:
      tags:
        - Tags
      summary: List tags
      parameters:
        - schema:
            type: integer
            minimum: 0
            exclusiveMinimum: true
          required: false
          name: limit
          in: query
        - schema:
            type: integer
            nullable: true
            minimum: 0
          required: false
          name: offset
          in: query
        - schema:
            type: string
            maxLength: 255
          required: false
          name: name
          in: query
      responses:
        '200':
          description: List of tags
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: number
                    minimum: 0
                  data:
                    type: object
                    properties:
                      tags:
                        type: array
                        items:
                          $ref: '#/components/schemas/Tag'
                    required:
                      - tags
                required:
                  - count
                  - data
    post:
      tags:
        - Tags
      summary: Create tag
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                description:
                  type: string
                  nullable: true
                resourceId:
                  type: string
                  nullable: true
                  pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
              required:
                - name
      responses:
        '201':
          description: Tag created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tag'
  /v2/upload:
    get:
      tags:
        - Uploads
      summary: Get upload URL
      description: Get a presigned URL for uploading a file.
      parameters:
        - schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            description: ID of the resource that will be overwritten by the uploaded file.
          required: false
          description: ID of the resource that will be overwritten by the uploaded file.
          name: resourceId
          in: query
        - schema:
            type: string
            minLength: 1
          required: true
          name: filename
          in: query
        - schema:
            type: integer
            nullable: true
            minimum: 0
          required: false
          name: size
          in: query
      responses:
        '200':
          description: Presigned data for uploading with a PUT request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresignedResponse'
  /v2/user/me:
    get:
      tags:
        - User profile
      summary: My profile
      responses:
        '200':
          description: My profile info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MineProfile'
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://fabric.so/oauth2/authorize
          tokenUrl: /oauth2/token
          refreshUrl: /oauth2/token
    ApiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: 'You can generate your API key under settings inside the Fabric app. Important: do not share or publish this key in your client-side code.'
  schemas:
    AssistantChat:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        name:
          type: string
          nullable: true
        isPinned:
          type: boolean
        createdAt:
          type: string
        modifiedAt:
          type: string
      required:
        - id
        - isPinned
        - createdAt
        - modifiedAt
    ChatbotModel:
      type: string
      nullable: true
      enum:
        - dummy
        - gpt-5-1
        - gpt-5-1-instant
        - gpt-4-1
        - gpt-4o
        - claude-haiku-4-5
        - claude-sonnet-4-5
        - claude-opus-4-5
        - gemini-2-5-flash
        - gemini-3-pro
        - grok-4
        - openai-o3
        - gpt-5
        - claude-sonnet-4-0
        - claude-opus-4-1
    ToolStatus:
      type: string
      enum:
        - success
        - failure
        - needs_confirmation
    AssistantMessagePart:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              enum:
                - text
            text:
              type: string
          required:
            - type
            - text
        - type: object
          properties:
            type:
              type: string
              enum:
                - tool
            name:
              type: string
            callId:
              type: string
            status:
              $ref: '#/components/schemas/ToolStatus'
            args:
              type: object
              additionalProperties:
                nullable: true
            result:
              nullable: true
            error:
              type: string
          required:
            - type
            - name
            - callId
            - status
            - args
    AssistantMessageBase:
      type: object
      properties:
        role:
          type: string
          enum:
            - user
            - assistant
        parts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/AssistantMessagePart'
          minItems: 1
        content:
          type: string
          nullable: true
          maxLength: 100000
      required:
        - role
    AssistantMessage:
      allOf:
        - $ref: '#/components/schemas/AssistantMessageBase'
        - type: object
          properties:
            id:
              type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            assistantChatId:
              type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            model:
              $ref: '#/components/schemas/ChatbotModel'
            context:
              type: object
              nullable: true
              properties:
                resourceIds:
                  type: array
                  items:
                    type: string
                    pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                snippets:
                  type: array
                  items:
                    type: string
            createdAt:
              type: string
            modifiedAt:
              type: string
          required:
            - id
            - assistantChatId
            - model
            - context
            - createdAt
            - modifiedAt
    UserGroup:
      type: string
      enum:
        - admin
        - fabric-internal
        - fabric-email
        - investor
        - dogfooding
        - google-drive-beta
        - beta-user-recap
        - local-file-sync
        - notepad-diffs
    UserProfilePublic:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        name:
          type: string
          nullable: true
          maxLength: 255
          pattern: ^[\p{Letter}\p{Emoji_Presentation}\p{Emoji_Modifier}0-9\s\-.']{1,255}$
        pictureUrlCdn:
          type: string
          nullable: true
          maxLength: 4096
      required:
        - id
    MineProfile:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        pictureUrlCdn:
          type: string
          nullable: true
          maxLength: 4096
        name:
          type: string
          nullable: true
          maxLength: 255
          pattern: ^[\p{Letter}\p{Emoji_Presentation}\p{Emoji_Modifier}0-9\s\-.']{1,255}$
        email:
          type: string
          maxLength: 255
          format: email
        authMethod:
          type: string
          nullable: true
          maxLength: 256
        groups:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/UserGroup'
        createdAt:
          type: string
        modifiedAt:
          type: string
        emailIngestionAddress:
          type: string
          nullable: true
          format: email
      required:
        - id
        - email
        - createdAt
        - modifiedAt
        - emailIngestionAddress
        - onboarding
    CommentCreate:
      type: object
      properties:
        content:
          type: string
          maxLength: 25000
    CommentWithAuthor:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        content:
          type: string
          maxLength: 25000
        createdAt:
          type: string
        modifiedAt:
          type: string
        resourceId:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        user:
          allOf:
            - $ref: '#/components/schemas/UserProfilePublic'
            - nullable: true
        mentionedUsers:
          type: array
          items:
            $ref: '#/components/schemas/UserProfilePublic'
      required:
        - id
        - content
        - annotation
        - createdAt
        - modifiedAt
        - resourceId
        - user
        - mentionedUsers
    IntegrationType:
      type: string
      enum:
        - GOOGLE_DRIVE
        - GMAIL
        - GITHUB
        - IOS_DEVICE
        - ANDROID_DEVICE
        - RSS
        - NOTION
        - LOCAL_FS
        - DROPBOX
        - ONEDRIVE
        - MIXPANEL
        - STRIPE
    IntegrationStatus:
      type: string
      enum:
        - ACTIVE
        - HIDDEN
        - INACTIVE
    PresignedImage:
      type: object
      properties:
        url:
          type: string
        width:
          type: number
          nullable: true
        height:
          type: number
          nullable: true
        mime:
          type: string
          nullable: true
      required:
        - url
    ProcessingState:
      type: string
      nullable: true
      enum:
        - pending
        - processing
        - completed
        - failed
      description: Status of the link preview
    EmailMessageMetadataBlock:
      type: object
      properties:
        emailMessage:
          type: object
          properties:
            snippet:
              type: string
              nullable: true
            subject:
              type: string
              nullable: true
            from:
              type: string
              nullable: true
            to:
              type: string
              nullable: true
            body:
              type: string
              nullable: true
            cc:
              type: string
              nullable: true
            bcc:
              type: string
              nullable: true
          required:
            - snippet
            - subject
            - from
            - to
            - body
            - cc
            - bcc
        emailPreview:
          type: object
          properties:
            html:
              type: string
              nullable: true
              description: HTML content of the email
          required:
            - html
        caption:
          type: string
          nullable: true
          description: Summary text
        keywords:
          type: array
          nullable: true
          items:
            type: string
          description: Keywords
      required:
        - emailMessage
        - caption
    PermissionRole:
      type: string
      enum:
        - owner
        - viewer
        - editor
        - commenter
    Permission:
      type: object
      properties:
        resourceId:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        userId:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        role:
          $ref: '#/components/schemas/PermissionRole'
        createdAt:
          type: string
        modifiedAt:
          type: string
      required:
        - resourceId
        - userId
        - role
        - createdAt
        - modifiedAt
    PermissionWithUser:
      allOf:
        - $ref: '#/components/schemas/Permission'
        - type: object
          properties:
            user:
              $ref: '#/components/schemas/UserProfilePublic'
          required:
            - user
    Kind:
      type: string
      enum:
        - default
        - image
        - document
        - folder
        - bookmark
        - highlight
        - notepad
        - video
        - audio
        - email
        - person
        - voicenote
    Tag:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        userId:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        name:
          type: string
          maxLength: 255
        description:
          type: string
          nullable: true
        createdAt:
          type: string
          nullable: true
        modifiedAt:
          type: string
          nullable: true
      required:
        - id
        - userId
        - name
        - description
    RootAlias:
      type: string
      enum:
        - '@alias::inbox'
        - '@alias::bin'
    FileCreation:
      type: object
      properties:
        name:
          type: string
          nullable: true
          minLength: 1
          maxLength: 255
        attachment:
          type: object
          properties:
            path:
              type: string
            filename:
              type: string
          required:
            - path
            - filename
        parentId:
          anyOf:
            - type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            - $ref: '#/components/schemas/RootAlias'
          description: Parent resource ID or an alias (the alias is resolved to the actual ID during the request handling).
        tags:
          type: array
          items:
            anyOf:
              - type: object
                properties:
                  id:
                    type: string
                    pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
                required:
                  - id
              - type: object
                properties:
                  name:
                    type: string
                    maxLength: 255
                required:
                  - name
        comment:
          allOf:
            - $ref: '#/components/schemas/CommentCreate'
            - nullable: true
        url:
          type: string
          format: uri
        mimeType:
          type: string
        metadata:
          type: object
          properties:
            voiceNote:
              type: boolean
              nullable: true
              description: Whether this item is the audio part of a voice note
            transcript:
              type: object
              nullable: true
              properties:
                key:
                  type: string
              required:
                - key
      required:
        - attachment
        - parentId
        - mimeType
    RootType:
      type: string
      enum:
        - SYSTEM
        - SPACE
        - INTEGRATION
    SystemSubtypes:
      type: string
      enum:
        - inbox
        - bin
    ResourceIcon:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              enum:
                - emoji
            emoji:
              type: string
          required:
            - type
            - emoji
        - type: object
          properties:
            type:
              type: string
              enum:
                - predefined
            predefined:
              type: string
          required:
            - type
            - predefined
        - type: object
          properties:
            type:
              type: string
              enum:
                - customImage
            customImage:
              $ref: '#/components/schemas/PresignedImage'
          required:
            - type
            - customImage
    FolderBase:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        name:
          type: string
          nullable: true
      required:
        - id
        - name
    Folder:
      allOf:
        - $ref: '#/components/schemas/FolderBase'
        - type: object
          properties:
            userId:
              type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            parentId:
              type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            isReadonly:
              type: boolean
            icon:
              allOf:
                - $ref: '#/components/schemas/ResourceIcon'
                - oneOf:
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - emoji
                        emoji:
                          type: string
                      required:
                        - type
                        - emoji
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - predefined
                        predefined:
                          type: string
                      required:
                        - type
                        - predefined
                    - type: object
                      properties:
                        type:
                          type: string
                          enum:
                            - customImage
                        customImage:
                          $ref: '#/components/schemas/PresignedImage'
                      required:
                        - type
                        - customImage
                    - nullable: true
          required:
            - userId
            - parentId
            - isReadonly
            - icon
    ResourceRootFolder:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        name:
          type: string
          nullable: true
        isReadonly:
          type: boolean
        icon:
          allOf:
            - $ref: '#/components/schemas/ResourceIcon'
            - oneOf:
                - type: object
                  properties:
                    type:
                      type: string
                      enum:
                        - emoji
                    emoji:
                      type: string
                  required:
                    - type
                    - emoji
                - type: object
                  properties:
                    type:
                      type: string
                      enum:
                        - predefined
                    predefined:
                      type: string
                  required:
                    - type
                    - predefined
                - type: object
                  properties:
                    type:
                      type: string
                      enum:
                        - customImage
                    customImage:
                      $ref: '#/components/schemas/PresignedImage'
                  required:
                    - type
                    - customImage
                - nullable: true
        permissions:
          type: object
          properties:
            role:
              $ref: '#/components/schemas/PermissionRole'
          required:
            - role
        childrenCount:
          type: number
        memberCount:
          type: number
          minimum: 0
      required:
        - id
        - name
        - isReadonly
        - icon
        - childrenCount
        - memberCount
    Resource:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        kind:
          $ref: '#/components/schemas/Kind'
        mimeType:
          type: string
          nullable: true
        extension:
          type: string
          nullable: true
        name:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        icon:
          allOf:
            - $ref: '#/components/schemas/ResourceIcon'
        size:
          type: number
          nullable: true
        url:
          type: string
        commentPinned:
          allOf:
            - $ref: '#/components/schemas/CommentWithAuthor'
            - nullable: true
        label:
          type: object
          nullable: true
          properties:
            id:
              type: number
            hexColor:
              type: string
              maxLength: 7
          required:
            - id
            - hexColor
        parent:
          type: object
          nullable: true
          properties:
            id:
              type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            name:
              type: string
              nullable: true
          required:
            - id
            - name
        cover:
          allOf:
            - $ref: '#/components/schemas/PresignedImage'
            - nullable: true
        user:
          allOf:
            - $ref: '#/components/schemas/UserProfilePublic'
            - nullable: true
        root:
          type: object
          nullable: true
          properties:
            id:
              type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            type:
              $ref: '#/components/schemas/RootType'
            subtype:
              anyOf:
                - $ref: '#/components/schemas/IntegrationType'
                - $ref: '#/components/schemas/SystemSubtypes'
                - nullable: true
            private:
              type: boolean
            folder:
              allOf:
                - $ref: '#/components/schemas/FolderBase'
                - type: object
                  properties:
                    user:
                      allOf:
                        - $ref: '#/components/schemas/UserProfilePublic'
                        - nullable: true
          required:
            - id
            - type
            - subtype
            - private
            - folder
        stateProcessing:
          $ref: '#/components/schemas/ProcessingState'
        originUrl:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
              name:
                type: string
                maxLength: 255
            required:
              - id
              - name
        chats:
          type: array
          items:
            $ref: '#/components/schemas/AssistantChat'
        thumbnail:
          type: object
          nullable: true
          properties:
            sm:
              type: string
            md:
              type: string
            lg:
              type: string
            xl:
              type: string
            original:
              type: string
        fileUrl:
          type: string
          nullable: true
        preferences:
          type: object
          nullable: true
          additionalProperties:
            nullable: true
        publishedAncestors:
          type: array
          items:
            type: object
            properties:
              resourceId:
                type: string
                pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
              accessToken:
                type: string
            required:
              - resourceId
              - accessToken
        createdAt:
          type: string
        modifiedAt:
          type: string
      required:
        - id
        - kind
        - mimeType
        - extension
        - name
        - description
        - size
        - url
        - commentCount
        - commentPinned
        - pins
        - pinnedTo
        - label
        - parent
        - ancestors
        - cover
        - user
        - root
        - stateProcessing
        - jobsProcessing
        - originUrl
        - tags
        - chats
        - thumbnail
        - fileUrl
        - preferences
        - isShared
        - isReadonly
        - isHidden
        - shareToken
        - publishToken
        - createdAt
        - modifiedAt
    ResourceDetailBookmark:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - bookmark
          required:
            - data
    ResourceDetailEmailMessage:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - email
            data:
              $ref: '#/components/schemas/EmailMessageMetadataBlock'
          required:
            - data
    ResourceDetailHighlight:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - highlight
          required:
            - data
    ResourceDetailDocument:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - document
          required:
            - data
    ResourceDetailFolder:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - folder
          required:
            - data
    ResourceDetailImage:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - image
          required:
            - data
    ResourceDetailVideo:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - video
          required:
            - data
    ResourceDetailAudio:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - audio
          required:
            - data
    ResourceDetailNotepad:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - notepad
          required:
            - data
    ResourceDetailVoicenote:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - voicenote
          required:
            - data
    ResourceDetailDefault:
      allOf:
        - $ref: '#/components/schemas/Resource'
        - type: object
          properties:
            kind:
              type: string
              enum:
                - default
          required:
            - data
    ResourceDetail:
      oneOf:
        - $ref: '#/components/schemas/ResourceDetailDocument'
        - $ref: '#/components/schemas/ResourceDetailAudio'
        - $ref: '#/components/schemas/ResourceDetailVideo'
        - $ref: '#/components/schemas/ResourceDetailBookmark'
        - $ref: '#/components/schemas/ResourceDetailFolder'
        - $ref: '#/components/schemas/ResourceDetailHighlight'
        - $ref: '#/components/schemas/ResourceDetailImage'
        - $ref: '#/components/schemas/ResourceDetailNotepad'
        - $ref: '#/components/schemas/ResourceDetailVoicenote'
        - $ref: '#/components/schemas/ResourceDetailEmailMessage'
        - $ref: '#/components/schemas/ResourceDetailDefault'
      discriminator:
        propertyName: kind
        mapping:
          document: '#/components/schemas/ResourceDetailDocument'
          audio: '#/components/schemas/ResourceDetailAudio'
          video: '#/components/schemas/ResourceDetailVideo'
          bookmark: '#/components/schemas/ResourceDetailBookmark'
          folder: '#/components/schemas/ResourceDetailFolder'
          highlight: '#/components/schemas/ResourceDetailHighlight'
          image: '#/components/schemas/ResourceDetailImage'
          notepad: '#/components/schemas/ResourceDetailNotepad'
          voicenote: '#/components/schemas/ResourceDetailVoicenote'
          email: '#/components/schemas/ResourceDetailEmailMessage'
          default: '#/components/schemas/ResourceDetailDefault'
    SubscriptionTier:
      type: string
      nullable: true
      enum:
        - free
        - basic
        - starter
        - pro
        - max
        - lifetime
        - team
    PlanEntitlements:
      type: object
      properties:
        maxFileSizeMB:
          type: integer
        chatbotCredits:
          type: integer
        maxResourceCount:
          type: integer
        maxTotalStorageSizeMB:
          type: integer
        creditCentsMonthly:
          type: integer
        integrationsEnabled:
          type: boolean
        bulkImportEnabled:
          type: boolean
        audioTranscriptionEnabled:
          type: boolean
        aiVoiceAndMeetingRecordingEnabled:
          type: boolean
        recapsEnabled:
          type: boolean
        modelSwitchingEnabled:
          type: boolean
        apiKeysEnabled:
          type: boolean
        fabricLabsEnabled:
          type: boolean
        freeImportItemLimit:
          type: integer
      required:
        - maxFileSizeMB
        - chatbotCredits
        - maxResourceCount
        - maxTotalStorageSizeMB
        - creditCentsMonthly
        - integrationsEnabled
        - bulkImportEnabled
        - audioTranscriptionEnabled
        - aiVoiceAndMeetingRecordingEnabled
        - recapsEnabled
        - modelSwitchingEnabled
        - apiKeysEnabled
        - fabricLabsEnabled
        - freeImportItemLimit
    BookmarkCreationResponse:
      anyOf:
        - $ref: '#/components/schemas/ResourceDetailBookmark'
        - $ref: '#/components/schemas/ResourceDetailDocument'
    PublicIntegrationSchema:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        userId:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        externalId:
          type: string
        email:
          type: string
          nullable: true
        displayName:
          type: string
        pictureUrl:
          type: string
          nullable: true
        type:
          $ref: '#/components/schemas/IntegrationType'
        createdAt:
          type: string
        modifiedAt:
          type: string
        status:
          $ref: '#/components/schemas/IntegrationStatus'
        statusMessages:
          type: object
          additionalProperties:
            nullable: true
        needsReinstall:
          type: boolean
      required:
        - id
        - userId
        - externalId
        - email
        - displayName
        - pictureUrl
        - type
        - createdAt
        - modifiedAt
        - status
        - statusMessages
        - needsReinstall
    UsageItem:
      type: object
      properties:
        limit:
          type: number
        used:
          type: number
      required:
        - limit
        - used
    UsageItems:
      type: object
      properties:
        storage:
          $ref: '#/components/schemas/UsageItem'
        chatbot:
          $ref: '#/components/schemas/UsageItem'
        credits:
          $ref: '#/components/schemas/UsageItem'
        imported_items:
          $ref: '#/components/schemas/UsageItem'
    SubscriptionStatus:
      type: string
      nullable: true
      enum:
        - active
        - canceled
        - incomplete
        - incomplete_expired
        - past_due
        - paused
        - trialing
        - unpaid
    BillingCycle:
      type: string
      nullable: true
      enum:
        - week
        - month
        - year
        - year_3
    Store:
      type: string
      nullable: true
      enum:
        - app_store
        - play_store
        - stripe
        - promotional
    WorkspaceSubscription:
      type: object
      properties:
        tier:
          $ref: '#/components/schemas/SubscriptionTier'
        status:
          $ref: '#/components/schemas/SubscriptionStatus'
        trialEndingAt:
          type: string
          nullable: true
        billingCycle:
          $ref: '#/components/schemas/BillingCycle'
        entitlements:
          allOf:
            - $ref: '#/components/schemas/PlanEntitlements'
            - nullable: true
        hasDefaultPaymentMethod:
          type: boolean
        usage:
          $ref: '#/components/schemas/UsageItems'
        creditTopupAt:
          type: string
          nullable: true
        creditOperationCosts:
          type: object
          properties:
            RESOURCE_CREATED:
              type: integer
            ASSISTANT_MESSAGE:
              type: integer
            TOP_UP_MANUAL:
              type: integer
            TOP_UP_PLAN:
              type: integer
            TOP_UP_AUTO:
              type: integer
        planStoreId:
          type: string
          nullable: true
        planStoreType:
          $ref: '#/components/schemas/Store'
      required:
        - tier
        - status
        - trialEndingAt
        - billingCycle
        - entitlements
        - hasDefaultPaymentMethod
        - usage
        - creditTopupAt
        - creditOperationCosts
        - planStoreId
        - planStoreType
    ResourceFilterResponse:
      type: object
      properties:
        total:
          type: number
        hasMore:
          type: boolean
        nextCursor:
          type: string
          nullable: true
        resources:
          type: array
          items:
            $ref: '#/components/schemas/ResourceDetail'
      required:
        - total
        - hasMore
        - resources
    ResourceRootBase:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        type:
          $ref: '#/components/schemas/RootType'
        createdAt:
          type: string
        modifiedAt:
          type: string
        private:
          type: boolean
        lastResourceAddedAt:
          type: string
          nullable: true
          description: The timestamp of when the last descendant resource was added
        folder:
          $ref: '#/components/schemas/ResourceRootFolder'
      required:
        - id
        - type
        - createdAt
        - modifiedAt
        - private
        - folder
    ResourceRootSystem:
      allOf:
        - $ref: '#/components/schemas/ResourceRootBase'
        - type: object
          properties:
            type:
              type: string
              enum:
                - SYSTEM
            subtype:
              $ref: '#/components/schemas/SystemSubtypes'
          required:
            - subtype
    ResourceRootIntegration:
      allOf:
        - $ref: '#/components/schemas/ResourceRootBase'
        - type: object
          properties:
            type:
              type: string
              enum:
                - INTEGRATION
            subtype:
              $ref: '#/components/schemas/IntegrationType'
          required:
            - subtype
    ResourceRootSpace:
      allOf:
        - $ref: '#/components/schemas/ResourceRootBase'
        - type: object
          properties:
            type:
              type: string
              enum:
                - SPACE
            subtype:
              type: object
              nullable: true
              enum:
                - null
          required:
            - subtype
    ResourceRootListItem:
      anyOf:
        - $ref: '#/components/schemas/ResourceRootSystem'
        - $ref: '#/components/schemas/ResourceRootIntegration'
        - $ref: '#/components/schemas/ResourceRootSpace'
    ResourceRootDetail:
      allOf:
        - type: object
          properties:
            id:
              type: string
              pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
            type:
              $ref: '#/components/schemas/RootType'
            createdAt:
              type: string
            modifiedAt:
              type: string
            private:
              type: boolean
            lastResourceAddedAt:
              type: string
              nullable: true
              description: The timestamp of when the last descendant resource was added
            folder:
              $ref: '#/components/schemas/ResourceRootFolder'
          required:
            - id
            - type
            - createdAt
            - modifiedAt
            - private
            - folder
        - oneOf:
            - $ref: '#/components/schemas/ResourceRootSystem'
            - $ref: '#/components/schemas/ResourceRootSpace'
            - allOf:
                - $ref: '#/components/schemas/ResourceRootIntegration'
                - type: object
    Space:
      type: object
      properties:
        id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
        name:
          type: string
          nullable: true
      required:
        - id
        - name
    PresignedResponse:
      type: object
      properties:
        url:
          type: string
        headers:
          anyOf:
            - type: object
              properties:
                ETag:
                  type: string
                  nullable: true
                Content-Disposition:
                  type: string
                  nullable: true
            - type: object
              additionalProperties:
                type: string
      required:
        - url
        - headers
    SharingFilters:
      type: string
      enum:
        - onlyMe
        - published
        - shared
        - sharedWithMe
        - sharedWithMeDirectly
    UsageType:
      type: string
      enum:
        - storage
        - chatbot
        - credits
        - imported_items
  parameters: {}

path: scripts/fabric.mjs
content: #!/usr/bin/env node
/**
 * Fabric API helper (Node.js)
 *
 * Cross-platform wrapper for calling the Fabric HTTP API without relying on bash.
 *
 * Usage:
 *   node scripts/fabric.mjs GET /v2/user/me
 *   node scripts/fabric.mjs POST /v2/notepads --json '{"name":"Hello","text":"World","parentId":"@alias::inbox"}'
 *   node scripts/fabric.mjs POST /v2/notepads --file payload.json
 *   cat payload.json | node scripts/fabric.mjs POST /v2/notepads
 *
 * Env:
 *   FABRIC_API_KEY  (required for API paths like /v2/..., unless --no-key)
 *   FABRIC_BASE     (optional, default: https://api.fabric.so)
 */

import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
import process from 'node:process';

function printUsage(exitCode = 2) {
  const msg = `Usage:
  node scripts/fabric.mjs <METHOD> <PATH_OR_URL> [options]

Examples:
  node scripts/fabric.mjs GET /v2/user/me

  node scripts/fabric.mjs POST /v2/notepads --json '{"name":"Test","text":"Hello","parentId":"@alias::inbox"}'

  node scripts/fabric.mjs POST /v2/notepads --file payload.json

  cat payload.json | node scripts/fabric.mjs POST /v2/notepads

Options:
  --base <url>          Base URL for API paths (default: $FABRIC_BASE or https://api.fabric.so)
  --json <string>       Send request body as a literal string (typically JSON)
  --file <path>         Send request body from a file (text by default)
  --raw                 With --file, send file bytes (octet-stream). Useful for presigned PUT uploads.
  --header "K: V"       Add a header (repeatable)
  --no-key              Do not attach X-Api-Key (useful for presigned URLs)
  --with-key            Force attaching X-Api-Key even for absolute URLs
  --pretty              Pretty-print JSON responses
  --no-pretty           Do not pretty-print JSON responses
  -h, --help            Show this help

Notes:
  * If <PATH_OR_URL> is an absolute URL (https://...), the script will NOT send X-Api-Key unless you pass --with-key.
  * For relative paths (e.g. /v2/notepads), the script requires FABRIC_API_KEY unless --no-key.
`;
  process.stderr.write(msg);
  process.exit(exitCode);
}

function isAbsoluteUrl(s) {
  return /^https?:\/\//i.test(s);
}

function joinBase(base, path) {
  const b = base.replace(/\/+$/, '');
  const p = path.startsWith('/') ? path : `/${path}`;
  return `${b}${p}`;
}

function parseHeaderLine(line) {
  const idx = line.indexOf(':');
  if (idx === -1) {
    throw new Error(`Invalid --header value (missing ':'): ${line}`);
  }
  const key = line.slice(0, idx).trim();
  const value = line.slice(idx + 1).trim();
  if (!key) throw new Error(`Invalid --header key: ${line}`);
  return [key, value];
}

async function readStdinAsBuffer() {
  const chunks = [];
  for await (const chunk of process.stdin) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
  }
  return Buffer.concat(chunks);
}

async function main() {
  const argv = process.argv.slice(2);
  if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
    printUsage(0);
  }

  const method = (argv[0] || '').toUpperCase();
  const target = argv[1];
  if (!method || !target) {
    printUsage(2);
  }

  let base = process.env.FABRIC_BASE || 'https://api.fabric.so';
  let jsonBody = null;
  let filePath = null;
  let raw = false;
  let noKey = false;
  let withKey = false;

  // Default pretty-print when stdout is a TTY
  let pretty = process.stdout.isTTY;

  const extraHeaders = [];

  for (let i = 2; i < argv.length; i += 1) {
    const a = argv[i];

    if (a === '--base') {
      base = argv[++i];
      if (!base) throw new Error('Missing value for --base');
      continue;
    }

    if (a === '--json') {
      jsonBody = argv[++i];
      if (jsonBody == null) throw new Error('Missing value for --json');
      continue;
    }

    if (a === '--file') {
      filePath = argv[++i];
      if (!filePath) throw new Error('Missing value for --file');
      continue;
    }

    if (a === '--raw') {
      raw = true;
      continue;
    }

    if (a === '--header') {
      const h = argv[++i];
      if (!h) throw new Error('Missing value for --header');
      extraHeaders.push(parseHeaderLine(h));
      continue;
    }

    if (a === '--no-key') {
      noKey = true;
      continue;
    }

    if (a === '--with-key') {
      withKey = true;
      continue;
    }

    if (a === '--pretty') {
      pretty = true;
      continue;
    }

    if (a === '--no-pretty') {
      pretty = false;
      continue;
    }

    // Unknown option
    throw new Error(`Unknown option: ${a}`);
  }

  const abs = isAbsoluteUrl(target);
  const url = abs ? target : joinBase(base, target);

  // Default: don't leak API keys to presigned URLs
  const shouldSendKey = !noKey && (!abs || withKey);

  const [REDACTED_SECRET];
  if (shouldSendKey && (!apiKey || apiKey.trim() === '')) {
    process.stderr.write('ERROR: FABRIC_API_KEY is not set.\n');
    process.stderr.write('Set it in the environment or via OpenClaw skills config (skills.entries.fabric-api.apiKey).\n');
    process.exit(2);
  }

  let body = undefined;
  let bodyIsStream = false;
  let inferredContentType = null;

  if (jsonBody != null) {
    body = jsonBody;
    inferredContentType = 'application/json';
  } else if (filePath != null) {
    if (raw) {
      body = fs.createReadStream(filePath);
      bodyIsStream = true;
      inferredContentType = 'application/octet-stream';
    } else {
      body = await readFile(filePath, 'utf8');
      inferredContentType = 'application/json';
    }
  } else if (!process.stdin.isTTY) {
    const buf = await readStdinAsBuffer();
    if (buf.length > 0) {
      // Assume JSON-by-default when piping into an API call.
      body = buf.toString('utf8');
      inferredContentType = 'application/json';
    }
  }

  const headers = new Headers();

  // User-provided headers first (so our defaults can be overridden if desired).
  for (const [k, v] of extraHeaders) {
    headers.set(k, v);
  }

  if (!headers.has('accept')) {
    headers.set('Accept', 'application/json');
  }

  if (shouldSendKey) {
    headers.set('X-Api-Key', apiKey);
  }

  if (body !== undefined && !headers.has('content-type') && inferredContentType) {
    headers.set('Content-Type', inferredContentType);
  }

  const fetchOpts = {
    method,
    headers,
    body,
  };

  // Node fetch requires this when streaming request bodies.
  if (bodyIsStream) {
    fetchOpts.duplex = 'half';
  }

  let res;
  try {
    res = await fetch(url, fetchOpts);
  } catch (err) {
    process.stderr.write(`ERROR: request failed: ${err?.message || String(err)}\n`);
    process.exit(1);
  }

  const text = await res.text();
  const contentType = res.headers.get('content-type') || '';
  const isJson = /(^|\s|;)application\/(json|[^;]+\+json)(;|\s|$)/i.test(contentType);

  if (!res.ok) {
    process.stderr.write(`HTTP ${res.status} ${res.statusText}\n`);
    if (text) process.stdout.write(text);
    process.exit(1);
  }

  if (!text) return;

  if (pretty && isJson) {
    try {
      const obj = JSON.parse(text);
      process.stdout.write(JSON.stringify(obj, null, 2));
      if (process.stdout.isTTY) process.stdout.write('\n');
      return;
    } catch {
      // Fall back to raw text
    }
  }

  process.stdout.write(text);
  if (process.stdout.isTTY) process.stdout.write('\n');
}

main().catch((err) => {
  process.stderr.write(`ERROR: ${err?.stack || err?.message || String(err)}\n`);
  process.exit(1);
});


path: scripts/fabric.py
content: #!/usr/bin/env python3
"""Fabric API helper (Python)

Cross-platform wrapper for calling the Fabric HTTP API without relying on bash.

Usage:
  python3 scripts/fabric.py GET /v2/user/me

  python3 scripts/fabric.py POST /v2/notepads --json '{"name":"Test","text":"Hello","parentId":"@alias::inbox"}'

  python3 scripts/fabric.py POST /v2/notepads --file payload.json

  type payload.json | python scripts/fabric.py POST /v2/notepads   # Windows cmd.exe

Env:
  FABRIC_API_KEY  (required for API paths like /v2/..., unless --no-key)
  FABRIC_BASE     (optional, default: https://api.fabric.so)

Notes:
  * If the target is an absolute URL (https://...), the script will NOT send X-Api-Key unless --with-key.
  * This script uses only Python's standard library (urllib).
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Dict, List, Tuple


def is_absolute_url(s: str) -> bool:
    return re.match(r"^https?://", s, re.IGNORECASE) is not None


def join_base(base: str, path: str) -> str:
    b = base.rstrip("/")
    p = path if path.startswith("/") else f"/{path}"
    return f"{b}{p}"


def parse_header_line(line: str) -> Tuple[str, str]:
    if ":" not in line:
        raise ValueError(f"Invalid --header value (missing ':'): {line}")
    key, value = line.split(":", 1)
    key = key.strip()
    value = value.strip()
    if not key:
        raise ValueError(f"Invalid --header key: {line}")
    return key, value


def has_header(headers: Dict[str, str], name: str) -> bool:
    name_l = name.lower()
    return any(k.lower() == name_l for k in headers.keys())


def main() -> int:
    parser = argparse.ArgumentParser(add_help=True)
    parser.add_argument("method", help="HTTP method, e.g. GET/POST/PUT/PATCH/DELETE")
    parser.add_argument("target", help="API path (e.g. /v2/notepads) or absolute URL (https://...)")
    parser.add_argument("--base", default=os.environ.get("FABRIC_BASE", "https://api.fabric.so"))
    parser.add_argument("--json", dest="json_body", default=None, help="Request body as literal string (typically JSON)")
    parser.add_argument("--file", dest="file_path", default=None, help="Request body from file")
    parser.add_argument("--raw", action="store_true", help="With --file, send raw bytes (octet-stream)")
    parser.add_argument("--header", action="append", default=[], help="Header line 'K: V' (repeatable)")
    parser.add_argument("--no-key", action="store_true", help="Do not attach X-Api-Key (useful for presigned URLs)")
    parser.add_argument("--with-key", action="store_true", help="Force attaching X-Api-Key even for absolute URLs")
    parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON responses")
    parser.add_argument("--no-pretty", action="store_true", help="Do not pretty-print JSON responses")

    args = parser.parse_args()

    method = (args.method or "").upper().strip()
    if not method:
        sys.stderr.write("ERROR: missing method\n")
        return 2

    abs_url = is_absolute_url(args.target)
    url = args.target if abs_url else join_base(args.base, args.target)

    # Default: don't leak API keys to presigned URLs
    should_send_key = (not args.no_key) and ((not abs_url) or args.with_key)

    [REDACTED_SECRET]"FABRIC_API_KEY", "").strip()
    if should_send_key and not [REDACTED_SECRET]"ERROR: FABRIC_API_KEY is not set.\n")
        sys.stderr.write("Set it in the environment or via OpenClaw skills config (skills.entries.fabric-api.apiKey).\n")
        return 2

    headers: Dict[str, str] = {}
    for h in args.header:
        k, v = parse_header_line(h)
        headers[k] = v

    if not has_header(headers, "Accept"):
        headers["Accept"] = "application/json"

    if should_send_key:
        headers["X-Api-Key"] = api_key

    body: bytes | None = None
    inferred_ct: str | None = None

    if args.json_body is not None:
        body = args.json_body.encode("utf-8")
        inferred_ct = "application/json"
    elif args.file_path is not None:
        p = Path(args.file_path)
        if args.raw:
            body = p.read_bytes()
            inferred_ct = "application/octet-stream"
        else:
            body = p.read_text(encoding="utf-8").encode("utf-8")
            inferred_ct = "application/json"
    elif not sys.stdin.isatty():
        data = sys.stdin.buffer.read()
        if data:
            body = data
            inferred_ct = "application/json"

    if body is not None and (not has_header(headers, "Content-Type")) and inferred_ct:
        headers["Content-Type"] = inferred_ct

    req = urllib.request.Request(url=url, data=body, method=method, headers=headers)

    try:
        with urllib.request.urlopen(req) as resp:
            resp_body = resp.read() or b""
            ct = resp.headers.get("Content-Type", "") or ""
            is_json = "json" in ct.lower()

            pretty = args.pretty or (sys.stdout.isatty() and (not args.no_pretty))
            if resp_body and pretty and is_json:
                try:
                    obj = json.loads(resp_body.decode("utf-8"))
                    sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False))
                    sys.stdout.write("\n")
                    return 0
                except Exception:
                    pass

            if resp_body:
                sys.stdout.buffer.write(resp_body)
                if sys.stdout.isatty():
                    sys.stdout.write("\n")
            return 0

    except urllib.error.HTTPError as e:
        err_body = e.read() or b""
        sys.stderr.write(f"HTTP {e.code} {e.reason}\n")
        if err_body:
            sys.stdout.buffer.write(err_body)
            if sys.stdout.isatty():
                sys.stdout.write("\n")
        return 1

    except Exception as e:
        sys.stderr.write(f"ERROR: request failed: {e}\n")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())


path: references/REFERENCE.md
content: # Fabric API skill reference

This file is extra detail for the `fabric-api` skill. Read it when you need the exact schema expectations or you’re debugging validation errors.

## Endpoint map (from `fabric-api.yaml`)

Creation:

- Notepads (“notes”): `POST /v2/notepads`
- Folders: `POST /v2/folders`
- Bookmarks: `POST /v2/bookmarks`
- Files: `POST /v2/files` (requires `/v2/upload` first)
- Tags: `POST /v2/tags`

Read / browse:

- Who am I: `GET /v2/user/me`
- Resource roots: `GET /v2/resource-roots`
- Get resource by ID: `GET /v2/resources/{resourceId}`
- List resources: `POST /v2/resources/filter`
- Search: `POST /v2/search`
- List tags: `GET /v2/tags`

Dangerous / destructive:

- Delete: `POST /v2/resource/delete`
- Recover: `POST /v2/resource/recover`

## “Notes” vs “notepads”

The Fabric HTTP API models a note-like item as a **resource of kind `notepad`** and exposes a dedicated create endpoint:

- Create: `POST /v2/notepads`

If you previously tried `POST /v2/notes` and got a 404, that matches the OpenAPI spec: there is no `/v2/notes` path defined.

## `parentId`: valid values and when aliases work

For create endpoints (`/v2/notepads`, `/v2/folders`, `/v2/bookmarks`, `/v2/files`), `parentId` is:

- a UUID (folder resource id), **or**
- one of the RootAlias enum values:
  - `@alias::inbox`
  - `@alias::bin`

This alias resolution happens “during request handling” (server-side). It’s a convenience for creates.

For *filtering/listing* (`POST /v2/resources/filter`), `parentId` is defined as a UUID pattern and **does not accept aliases**. Use a UUID there.

### How to resolve Inbox/Bin IDs

Call:

- `GET /v2/resource-roots`

Find the root with `type: SYSTEM` and `subtype: inbox` or `subtype: bin`, then use `folder.id` as the UUID.

## Tags: exact schema (and the common failure modes)

Many create endpoints share the same `tags` schema:

```json
"tags": [
  { "name": "ideas" },
  { "id": "550e8400-e29b-41d4-a716-446655440000" }
]
```

Key rules:

- `tags` is an array.
- Each item must be an object.
- Each object must be either:
  - `{ "name": "<string up to 255>" }` (create or reuse tag by name), or
  - `{ "id": "<uuid>" }` (attach an existing tag by id)
- Don’t send:
  - `["tag1","tag2"]` (strings are invalid)
  - `[["tag1"],["tag2"]]` (nested arrays are invalid)
  - `{ "tags": { ... } }` (wrong shape)
  - `{ "name": ["ideas"] }` (wrong type)

If you’re unsure, omit `tags` for the initial create request. Then:

1) List tags (`GET /v2/tags?name=...`) or create tag (`POST /v2/tags`)  
2) Use tag IDs or names in a follow-up create/attach flow (Fabric currently supports tags on create; attaching after create may require an endpoint not in this spec).

## Notepad creation schema in plain English

`POST /v2/notepads` body is roughly:

- Required:
  - `parentId`
  - and either:
    - `text` (markdown string) **OR**
    - `ydoc` (structured)
- Optional:
  - `name` (nullable, 1-255 chars)
  - `tags` (array, shape above)
  - `comment` (object with `content`)

Minimal valid example:

```json
{
  "parentId": "@alias::inbox",
  "text": "Hello"
}
```

Recommended example:

```json
{
  "name": "My note title",
  "parentId": "@alias::inbox",
  "text": "# Heading\n\nSome text\n"
}
```

### `name` vs `title`

The schema uses `name`. If you have a “title” value (from a UI or user phrasing), map it to `name` in the payload.

## File upload (3-step flow)

Creating a file is a two-endpoint flow:

1) `GET /v2/upload?filename=...&size=...` → returns presigned `url` and required `headers`
2) `PUT` the file bytes to the presigned URL (no API key)
3) `POST /v2/files` with:
   - `attachment.path`: the path part of the presigned URL (strip host + query string)
   - `attachment.filename`: original filename
   - `parentId`: uuid or alias
   - `mimeType`

See the examples in `SKILL.md` and consult `fabric-api.yaml` for the exact `FileCreation` schema.

## Keep the OpenAPI close

When debugging, open `{baseDir}/fabric-api.yaml` and search for the exact endpoint:

- `paths: /v2/notepads`
- `paths: /v2/resources/filter`
- `components: schemas: RootAlias`, `FileCreation`, etc.

That spec is the source of truth for field names and required properties.


path: references/TROUBLESHOOTING.md
content: # Troubleshooting Fabric API requests

This is a quick playbook for debugging the most common failures when using the Fabric HTTP API via this skill.

## Always keep error bodies visible

When debugging schema validation (400s), the response body is usually the fastest way to fix the payload.

- If you use the Node/Python helpers shipped with this skill:
  - They print `HTTP <code> <reason>` to stderr on errors **and still print the response body**, then exit non‑zero.
  - Node: `node {baseDir}/scripts/fabric.mjs ...`
  - Python: `python3 {baseDir}/scripts/fabric.py ...`

- If you use raw `curl`, prefer:
  - `--fail-with-body` (shows 4xx/5xx bodies; newer curl)
  - `-sS` (silent, but still prints errors)

## 400 Bad Request

Almost always schema validation.

Checklist:

- Are you using the correct endpoint?
  - Create “note” → `POST /v2/notepads` (not `/v2/notes`)
- Did you include all required fields?
  - Notepad create: `parentId` and (`text` or `ydoc`)
  - Bookmark create: `url` and `parentId`
  - Folder create: `parentId` (and `name` if you want one)
- Is `parentId` valid?
  - UUID or `@alias::inbox` / `@alias::bin` for create endpoints
- Are `tags` shaped correctly?
  - `tags: [{"name":"x"},{"id":"<uuid>"}]`

### Common tag mistakes

- `tags: ["a","b"]` → invalid (strings)
- `tags: [["a"],["b"]]` → invalid (nested arrays)
- `tags: [{ "name": ["a"] }]` → invalid (name must be string)

## 401 Unauthorized / 403 Forbidden

- 401 usually means the API key is missing/invalid.
- 403 usually means:
  - the key lacks access to the resource (workspace permissions), or
  - your plan/subscription hit a limit.

Action:

- Stop and surface the `detail` / error message to the user.
- Do **not** brute-force retries.

## 404 Not Found

Most commonly:

- Wrong path (`/v2/notes` vs `/v2/notepads`)
- Wrong `resourceId`
- Wrong `parentId` (UUID doesn’t exist / you don’t have access)
- Trying to use an alias where only UUID is accepted (e.g. `POST /v2/resources/filter`)

## 429 Too Many Requests

Treat as rate limiting.

Recommended behaviour:

- Back off with jitter (sleep a bit, try again).
- Retry **reads** safely.
- Avoid automatic retries on creates unless you’ve built idempotency/dedupe checks (otherwise you can create duplicates).

## 5xx Server errors

Likely transient:

- Retry with exponential backoff.
- If repeatable, capture the full response body and report.

## Debugging “parentId alias” issues

If `@alias::inbox` works for creates but you need the actual UUID (e.g. to list children):

1) `GET /v2/resource-roots`
2) Find the SYSTEM inbox root
3) Use its `folder.id` as the UUID for `POST /v2/resources/filter`

## Capture a minimal repro payload

When you hit validation errors:

1) Remove optional fields until it works (start with just required fields)
2) Add fields back one by one:
   - first `name`
   - then `tags`
   - then `comment`

This makes it obvious which field is breaking schema validation.


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)