[
  {
    "slug": "zendesk",
    "version": "0.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/zendesk/0.1.2/eeaa01f48987ddbd4f09c660bccae5a91164885c61201110e710a5bc3bec279e.mjs",
    "digest": "eeaa01f48987ddbd4f09c660bccae5a91164885c61201110e710a5bc3bec279e",
    "contract_version": 1,
    "display_name": "Zendesk",
    "description": "Read-only Zendesk evidence for ecommerce late-order and return reviews.",
    "category": "Business",
    "auth": {
      "type": "token",
      "provider": "zendesk",
      "label": "Zendesk OAuth access token",
      "hint": "Attach a zendesk credential containing secret_accessToken. Multi-customer distributions require an approved Zendesk global OAuth client. The current OrgSDK host accepts this as a manually managed token; automatic Zendesk OAuth connection is not available."
    },
    "marketplace": {
      "displayName": "Zendesk",
      "category": "Business",
      "tagline": "Read evidence for late orders and return recommendations.",
      "tags": [
        "ecommerce",
        "support",
        "readonly",
        "exceptions"
      ],
      "useCases": [
        "Correlate order, shipment and support references",
        "Prepare evidence for a human-reviewed recommendation"
      ],
      "permissionsSummary": "Read operations only. No purchases, refunds, messages, updates or credential mutation. Account credentials may independently have broader rights.",
      "links": {
        "homepage": "https://orgsdk.ai",
        "docs": "https://developer.zendesk.com/api-reference/ticketing/introduction/"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "3150dd32bd9766acd4889973b342384e24a7bcb8",
      "notices_url": "https://orgsdk.ai/plugins/packages/zendesk/0.1.2/eeaa01f48987ddbd4f09c660bccae5a91164885c61201110e710a5bc3bec279e.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# OrgSDK Zendesk evidence plugin\n\nApache-2.0 read-only plugin for ticket and comment evidence using Ticketing API v2.\nNo ticket creation, updates, messages, attachment downloads or refunds.\n\n## Install in OrgSDK\n\nEnable the pinned Zendesk release from your project's Plugins page. The default\ncatalogue is https://orgsdk.ai/plugins/releases.json. The executor verifies the\nbundle SHA256. Installing npm files alone does not register a project plugin.\nSee LICENSE, NOTICE and TRADEMARKS.md for licensing and attribution.\n\n## Credentials and configuration\n\n1. Obtain an OAuth access token through your own Zendesk OAuth application. Use\n   an approved global OAuth client when serving multiple Zendesk customers;\n   internal clients are for internal use. Publishing this package does not grant\n   provider approval or implement a multi-customer consent service.\n2. Store provider `zendesk` with `secret_accessToken` in OrgSDK and attach that\n   credential to the plugin. Requests use `Authorization: Bearer`, never\n   email/API-token authentication. On OrgSDK 0.1.0, obtain and renew tokens\n   externally; native Zendesk onboarding and refresh are not implemented.\n3. Configure `{ \"subdomain\": \"your-account\" }`. This must be one lowercase DNS\n   label, without scheme, suffix, path or port. Optional `timeoutMs` bounds\n   requests. Secrets belong in credentials, not configuration.\n4. Call `zendesk.testConnection({})` to inspect the authenticated user. Role and\n   endpoint permissions still determine which tickets and comments are visible.\n\nThe fixed account host and attached credential determine access. No arbitrary\nURL, account override, token exchange, credential mutation or automatic retry\nis exposed. Provider error bodies and credentials are omitted from errors.\n\n## Named functions\n\n| Function             | Arguments                   | Result                                |\n| -------------------- | --------------------------- | ------------------------------------- |\n| `testConnection`     | `{}`                        | authenticated user identity           |\n| `listTickets`        | `{pageSize?, after?}`       | cursor page of ticket evidence        |\n| `getTicket`          | `{id}`                      | one exact ticket with matching ID     |\n| `searchTickets`      | `{query, page?, pageSize?}` | ticket-only candidate matches         |\n| `listTicketComments` | `{id, pageSize?, after?}`   | cursor page of comments/private notes |\n\nTicket `id` is a positive decimal string. `pageSize` is 1–100 (default 25),\n`after` is a bounded opaque cursor. Cursor responses provide `hasMore` and\n`after`; continue only when `hasMore`. Server pagination URLs are never followed.\nTicket listing sorts by latest update. Archived/role-invisible tickets may be\nabsent; list results are not a complete account export.\n\nSearch is read-only Zendesk syntax up to 512 characters with `type:ticket`\nprepended; non-ticket results are also removed. Search uses explicit numbered\npages while their starting offset is below 1,000 (40 pages at the default 25).\nThe response reports `hasMore`, `truncated` when no next page can be fetched, and\n`totalExceedsSearchLimit` when the reported count exceeds the limit. Zendesk caps\nresults at 1,000 per query; split by date/filter to inspect\nlarger populations. New/updated records can take minutes to appear. Search is\ncandidate evidence and never establishes a cross-service match by itself.\n\nTicket projections retain IDs, external ID, subject, bounded description,\nstatus/priority/type, timestamps, participant IDs, tags and custom fields for\nmerchant-defined order correlation. Description and each plain comment body are\ncapped at 20,000 characters with truncation flags. Comments can include private\ninternal notes. Up to 20 attachment metadata records (ID/name/type/size) per\ncomment are returned, with a truncation flag; content URLs and HTML are excluded.\nNo attachment is downloaded. An empty plain body may represent a non-text comment.\n\n## Official references\n\n- [Current authentication, API-token deprecation and global OAuth rule](https://developer.zendesk.com/api-reference/introduction/security-and-auth/)\n- [Tickets and role/list limits](https://developer.zendesk.com/api-reference/ticketing/tickets/tickets/)\n- [Search and 1,000-result limit](https://developer.zendesk.com/api-reference/ticketing/ticket-management/search/)\n- [Cursor pagination](https://developer.zendesk.com/documentation/api-basics/pagination/paginating-through-lists-using-cursor-pagination/)\n- [Ticket comments and private notes](https://developer.zendesk.com/api-reference/ticketing/tickets/ticket_comments/)\n\nTests use synthetic provider fixtures. Live account permissions and app approval must be verified with the operator’s selected account.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "subdomain": {
          "type": "string",
          "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$"
        },
        "timeoutMs": {
          "default": 15000,
          "type": "integer",
          "minimum": 100,
          "maximum": 30000
        }
      },
      "required": [
        "subdomain"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "testConnection",
        "description": "Read the authenticated user's ID, role and account-local email.",
        "calling": "named",
        "parameters": [],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "zendesk"
        ],
        "metadata": {
          "readonly": true,
          "category": "zendesk",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "listTickets",
        "description": "Read one cursor page of visible account tickets, most recently updated first.",
        "calling": "named",
        "parameters": [
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          },
          {
            "name": "after",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "zendesk"
        ],
        "metadata": {
          "readonly": true,
          "category": "zendesk",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "getTicket",
        "description": "Read one exact ticket with correlation fields and a bounded description.",
        "calling": "named",
        "parameters": [
          {
            "name": "id",
            "type": "string",
            "description": "Exact resource ID from this account."
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "zendesk"
        ],
        "metadata": {
          "readonly": true,
          "category": "zendesk",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "searchTickets",
        "description": "Read up to 1,000 ticket search matches across explicit pages. Search is eventually consistent; verify order references before correlating.",
        "calling": "named",
        "parameters": [
          {
            "name": "query",
            "type": "string"
          },
          {
            "name": "page",
            "type": "number",
            "optional": true
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "zendesk"
        ],
        "metadata": {
          "readonly": true,
          "category": "zendesk",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "listTicketComments",
        "description": "Read one cursor page of public comments and private notes for a ticket. Plain text bodies are capped; attachments are references only and never fetched.",
        "calling": "named",
        "parameters": [
          {
            "name": "id",
            "type": "string",
            "description": "Exact resource ID from this account."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          },
          {
            "name": "after",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "zendesk"
        ],
        "metadata": {
          "readonly": true,
          "category": "zendesk",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      }
    ]
  },
  {
    "slug": "xero",
    "version": "0.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/xero/0.1.2/2ac9bff24f86a1062100f6447c07d8e52fafe2fdd6a31338e83aefd9ea9ec528.mjs",
    "digest": "2ac9bff24f86a1062100f6447c07d8e52fafe2fdd6a31338e83aefd9ea9ec528",
    "contract_version": 1,
    "display_name": "Xero Readiness",
    "description": "Read Xero evidence for invoice readiness and unbilled-work review without changing provider records.",
    "category": "Finance",
    "auth": {
      "type": "token",
      "provider": "xero-oauth",
      "label": "Xero OAuth access token",
      "hint": "Attach an xero-oauth credential containing secret_accessToken and configure tenantId. On OrgSDK 0.1.0 this is a manually managed OAuth access token; renew it externally before expiry. Native Xero OAuth onboarding requires a future host release.",
      "scopes": [
        "offline_access",
        "accounting.invoices.read",
        "accounting.contacts.read",
        "accounting.attachments.read"
      ]
    },
    "marketplace": {
      "displayName": "Xero Readiness",
      "category": "Finance",
      "tagline": "Read Xero source evidence for reviewed invoice drafts.",
      "tags": [
        "readonly",
        "invoicing",
        "accounting"
      ],
      "useCases": [
        "Compare project/time evidence with existing invoices",
        "Prepare an invoice-readiness exception list",
        "Build reviewed drafts in OrgSDK without writing to the provider"
      ],
      "permissionsSummary": "Reads the configured authorised Xero organisation only: invoices/bills, contacts and invoice attachment metadata. Every call verifies the tenant against /connections. No writes, email, payments, deletion or file downloads.",
      "links": {
        "homepage": "https://orgsdk.ai",
        "docs": "https://developer.xero.com/documentation/api/accounting/invoices"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "28808806f683af5fcf363c96083cac88c80183e5",
      "notices_url": "https://orgsdk.ai/plugins/packages/xero/0.1.2/2ac9bff24f86a1062100f6447c07d8e52fafe2fdd6a31338e83aefd9ea9ec528.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Xero invoice readiness for OrgSDK\n\nApache-2.0 external plugin. Reads one explicitly configured Xero organisation to supply source evidence for reviewed drafts in OrgSDK. It never creates, edits, sends, authorises, voids, deletes, uploads, downloads files, or pays invoices.\n\n## Install in OrgSDK\n\nOpen your project's Plugins page, select this plugin and enable the pinned release.\nThe default catalogue is https://orgsdk.ai/plugins/releases.json. The executor\ndownloads the versioned bundle and verifies its SHA256. Configure the provider\nand attach the credential described below before running an action.\n\nThe plugin is Apache-2.0 licensed; see LICENSE, NOTICE and TRADEMARKS.md.\nThe generated npm distribution is a separate publication surface. Installing npm\nfiles alone does not register a project plugin. Local builds do not publish it.\n\n## Setup\n\n1. Obtain an OAuth access token with your own Xero application outside OrgSDK. On the released OrgSDK 0.1.0 host, Xero tokens must be managed and renewed externally; native Xero consent and refresh are not available in that version.\n2. Grant `accounting.invoices.read accounting.contacts.read accounting.attachments.read`. If the external token manager needs refresh tokens, also request `offline_access`. This plugin performs no token exchange or refresh.\n3. Attach the resulting `xero-oauth` credential to the plugin. The runtime injects `{ \"xero-oauth\": { \"secret_accessToken\": \"...\" } }`; never put access/refresh/client secrets in plugin configuration, action arguments or source.\n4. Use trusted setup tooling outside this plugin to read `GET https://api.xero.com/connections` with your authorised Xero connection. Identify the intended `ORGANISATION` and manually configure its exact `tenantId` UUID. There is no tenant chooser in the current platform. Keep the access token in the setup credential; never put it in OrgSDK action arguments. Example plugin configuration:\n\n   ```json\n   { \"tenantId\": \"70784a63-d24b-46a9-a4db-0e70a274b056\", \"timeoutMs\": 15000 }\n   ```\n\n5. Run `xero.verifyConnection({})`. It returns only the selected connected `ORGANISATION`. Every accounting read checks `/connections` again and rejects an unconnected or different tenant. Tenant IDs cannot be overridden through tool arguments.\n\n## Tools and review workflow\n\nAll calls use one named argument object, and return provider-style projected records. Exact TypeScript declarations ship with each tool.\n\n| Function                 | Arguments                                                    | Evidence                                                                                                         |\n| ------------------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |\n| `verifyConnection`       | `{}`                                                         | Selected connection and organisation name                                                                        |\n| `listInvoices`           | `{ from, to, status?, type?, contactId?, page?, pageSize? }` | Invoice/bill IDs, numbers, references, status, dates, currency, totals, due/paid/credited amounts and line items |\n| `getInvoice`             | `{ invoiceId }`                                              | One invoice or bill, with its available line items                                                               |\n| `listContacts`           | `{ page?, pageSize? }`                                       | Contact identities and billing addresses                                                                         |\n| `getContact`             | `{ contactId }`                                              | One contact's billing identity                                                                                   |\n| `listInvoiceAttachments` | `{ invoiceId }`                                              | File ID, name, MIME type and size; no URLs or bytes                                                              |\n\n`from`/`to` are inclusive invoice dates in `YYYY-MM-DD`, ordered and at most 366 calendar days. `type` is `ACCREC` (sales invoice) or `ACCPAY` (purchase bill). Optional status is `DRAFT`, `SUBMITTED`, `AUTHORISED`, `PAID`, `VOIDED` or `DELETED`. IDs must be UUIDs. Unknown arguments are rejected.\n\n```ts\nconst connection = await xero.verifyConnection({});\nconst existing = await xero.listInvoices({\n\tfrom: \"2026-10-01\",\n\tto: \"2026-10-30\",\n\ttype: \"ACCREC\",\n\tpageSize: 50,\n});\n// Match external work references to InvoiceNumber/Reference/LineItems.\n// Record ambiguities and missing evidence in an OrgSDK review draft.\n```\n\nA list call fetches one page only, `page` 1..10000 and `pageSize` 1..100 (default 50). `nextPage` is a conservative continuation when a full page is returned; `mayHaveMore` explicitly marks uncertainty. A final full page may require one empty request. If `mayHaveMore` is true with `nextPage` null, the page ceiling was reached: narrow the date window or contact filter and do not claim a complete reconciliation. Mutable source records can shift between pages, so this is not a transactional snapshot.\n\nProvider text is untrusted source content. Totals retain Xero's currency and line amount type; do not combine currencies or infer tax treatment. Documented numeric-string amounts are validated and normalized to numbers; optional fields preserve provider omission and nulls. Contact bank details and public invoice URLs are excluded. Attachment metadata is capped at 100 records and requires the separate attachment scope. Attachment download is intentionally absent in this release.\n\n## Boundaries and validation\n\nOnly `https://api.xero.com/connections` and allowlisted Accounting API invoice/contact GET paths are reachable. Redirects are rejected, bodies are streamed under a 2 MiB ceiling, each HTTP request has a 15s default deadline (configurable 1..30s), pages and nested arrays are bounded. A tool does at most two sequential HTTP reads, so the overall deadline is at most twice the configured request timeout. Errors contain safe status/context, never raw provider bodies, access tokens, credential values or network errors. No automatic retries conceal rate limits. Secrets have no environment-variable fallback.\n\n```sh\nbun install --frozen-lockfile\nbun run check\nbun run typecheck\nbun run test\nbun run validate\nbun run package\nbun run version-guard\n```\n\n`bun run package` creates the authoring SDK artifact in `dist/`. For the active local runtime, use the separate executable ESM packager from a platform checkout:\n\n```sh\nbun --no-env-file /absolute/path/to/orgsdk-platform/runtime/src/cli.ts package-plugin \\\n  --directory \"$PWD\" --output \"$PWD/release\" --license \"$PWD/LICENSE\"\n```\n\nThe `release/` result is self-contained and suitable for the active runtime's external plugin loader; it is distinct from the historical catalog artifact in `dist/`. Both outputs are ignored by Git. Package twice and compare hashes before distribution. The source repository carries a gated npm publication workflow. Publication and catalogue deployment are separate from local builds.\n\nTests use synthetic representative fixtures and mocked HTTP transport. No customer credentials or live Xero account has been used. Unit/contract checks do not establish provider account permissions, live OAuth acceptance or production readiness.\n\n## Official references\n\nVerified 30 October 2026:\n\n- [Current scopes and granular mapping](https://developer.xero.com/documentation/guides/oauth2/scopes/): `accounting.invoices.read` covers GET invoice resources, including bills; contacts and attachments keep their separate read scopes.\n- [Broad scope deprecation changelog](https://developer.xero.com/changelog): new granular scopes replace `accounting.transactions.read`.\n- [Auth code flow](https://developer.xero.com/documentation/guides/oauth2/auth-flow/) and [token refresh](https://developer.xero.com/documentation/guides/oauth2/token-types/).\n- [Tenants and `/connections`](https://developer.xero.com/documentation/guides/oauth2/tenants/).\n- [Invoices and attachments](https://developer.xero.com/documentation/api/accounting/invoices) and [contacts](https://developer.xero.com/documentation/api/accounting/contacts).\n- [Official Accounting OpenAPI](https://github.com/XeroAPI/Xero-OpenAPI/blob/master/xero_accounting.yaml), used for paths, fields and paging. Its legacy transaction scope annotations lag the current scopes page; the current granular scope documentation governs setup.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "tenantId": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
          "description": "Exact Xero organisation tenant ID obtained using trusted authorised /connections setup tooling. Configure it explicitly."
        },
        "timeoutMs": {
          "default": 15000,
          "type": "integer",
          "minimum": 1000,
          "maximum": 30000
        }
      },
      "required": [
        "tenantId"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "verifyConnection",
        "description": "Verify the configured tenant against Xero's authorised connections; return only that organisation.",
        "parameters": [],
        "returnType": "Promise<XeroConnection>",
        "typeDeclarations": "\ninterface XeroContact {\n ContactID: string; Name: string; ContactStatus?: string | null;\n AccountNumber?: string | null; ContactNumber?: string | null; FirstName?: string | null;\n LastName?: string | null; EmailAddress?: string | null; IsSupplier?: boolean;\n IsCustomer?: boolean; DefaultCurrency?: string | null; TaxNumber?: string | null;\n Addresses?: { AddressType?: string | null; AddressLine1?: string | null; AddressLine2?: string | null;\n AddressLine3?: string | null; AddressLine4?: string | null; City?: string | null;\n Region?: string | null; PostalCode?: string | null; Country?: string | null }[];\n}\ninterface XeroInvoice {\n InvoiceID: string; Type: \"ACCREC\" | \"ACCPAY\"; Status: string;\n InvoiceNumber?: string | null; Reference?: string | null;\n Contact?: { ContactID: string; Name?: string | null };\n Date?: string | null; DateString?: string | null; DueDate?: string | null; DueDateString?: string | null;\n CurrencyCode?: string | null; LineAmountTypes?: string | null; SubTotal?: number | null;\n TotalTax?: number | null; Total?: number | null; AmountDue?: number | null;\n AmountPaid?: number | null; AmountCredited?: number | null; FullyPaidOnDate?: string | null;\n UpdatedDateUTC?: string | null; SentToContact?: boolean; HasAttachments?: boolean;\n LineItems?: { LineItemID?: string; Description?: string | null; Quantity?: number | null;\n UnitAmount?: number | null; LineAmount?: number | null; TaxAmount?: number | null;\n TaxType?: string | null; AccountCode?: string | null; AccountID?: string | null;\n ItemCode?: string | null; DiscountRate?: number | null;\n Tracking?: { Name?: string | null; Option?: string | null }[] }[];\n}\ninterface XeroAttachment {\n AttachmentID: string; FileName: string; MimeType?: string | null; ContentLength?: number; IncludeOnline?: boolean;\n}\ninterface XeroConnection {\n id: string; tenantId: string; tenantType: string; tenantName?: string | null;\n createdDateUtc?: string | null; updatedDateUtc?: string | null;\n}\n",
        "requiredCredentials": [
          "xero-oauth"
        ],
        "metadata": {
          "category": "accounting",
          "readonly": true,
          "tags": [
            "readonly",
            "invoice-readiness"
          ]
        }
      },
      {
        "name": "listInvoices",
        "description": "Read one page of sales invoices or bills with line items in a date range of at most 366 days. No provider changes.",
        "parameters": [
          {
            "name": "from",
            "type": "string",
            "description": "Inclusive invoice date YYYY-MM-DD."
          },
          {
            "name": "to",
            "type": "string",
            "description": "Inclusive invoice date YYYY-MM-DD."
          },
          {
            "name": "status",
            "type": "\"DRAFT\" | \"SUBMITTED\" | \"AUTHORISED\" | \"PAID\" | \"VOIDED\" | \"DELETED\"",
            "optional": true
          },
          {
            "name": "type",
            "type": "\"ACCREC\" | \"ACCPAY\"",
            "optional": true
          },
          {
            "name": "contactId",
            "type": "string",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "optional": true,
            "description": "1..10000; one page is fetched per call."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true,
            "description": "1..100; defaults to 50."
          }
        ],
        "returnType": "Promise<{ invoices: XeroInvoice[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean }>",
        "typeDeclarations": "\ninterface XeroContact {\n ContactID: string; Name: string; ContactStatus?: string | null;\n AccountNumber?: string | null; ContactNumber?: string | null; FirstName?: string | null;\n LastName?: string | null; EmailAddress?: string | null; IsSupplier?: boolean;\n IsCustomer?: boolean; DefaultCurrency?: string | null; TaxNumber?: string | null;\n Addresses?: { AddressType?: string | null; AddressLine1?: string | null; AddressLine2?: string | null;\n AddressLine3?: string | null; AddressLine4?: string | null; City?: string | null;\n Region?: string | null; PostalCode?: string | null; Country?: string | null }[];\n}\ninterface XeroInvoice {\n InvoiceID: string; Type: \"ACCREC\" | \"ACCPAY\"; Status: string;\n InvoiceNumber?: string | null; Reference?: string | null;\n Contact?: { ContactID: string; Name?: string | null };\n Date?: string | null; DateString?: string | null; DueDate?: string | null; DueDateString?: string | null;\n CurrencyCode?: string | null; LineAmountTypes?: string | null; SubTotal?: number | null;\n TotalTax?: number | null; Total?: number | null; AmountDue?: number | null;\n AmountPaid?: number | null; AmountCredited?: number | null; FullyPaidOnDate?: string | null;\n UpdatedDateUTC?: string | null; SentToContact?: boolean; HasAttachments?: boolean;\n LineItems?: { LineItemID?: string; Description?: string | null; Quantity?: number | null;\n UnitAmount?: number | null; LineAmount?: number | null; TaxAmount?: number | null;\n TaxType?: string | null; AccountCode?: string | null; AccountID?: string | null;\n ItemCode?: string | null; DiscountRate?: number | null;\n Tracking?: { Name?: string | null; Option?: string | null }[] }[];\n}\ninterface XeroAttachment {\n AttachmentID: string; FileName: string; MimeType?: string | null; ContentLength?: number; IncludeOnline?: boolean;\n}\ninterface XeroConnection {\n id: string; tenantId: string; tenantType: string; tenantName?: string | null;\n createdDateUtc?: string | null; updatedDateUtc?: string | null;\n}\n",
        "requiredCredentials": [
          "xero-oauth"
        ],
        "metadata": {
          "category": "accounting",
          "readonly": true,
          "tags": [
            "readonly",
            "invoice-readiness"
          ]
        }
      },
      {
        "name": "getInvoice",
        "description": "Read one invoice or bill, including reference, totals, status and line items.",
        "parameters": [
          {
            "name": "invoiceId",
            "type": "string"
          }
        ],
        "returnType": "Promise<XeroInvoice>",
        "typeDeclarations": "\ninterface XeroContact {\n ContactID: string; Name: string; ContactStatus?: string | null;\n AccountNumber?: string | null; ContactNumber?: string | null; FirstName?: string | null;\n LastName?: string | null; EmailAddress?: string | null; IsSupplier?: boolean;\n IsCustomer?: boolean; DefaultCurrency?: string | null; TaxNumber?: string | null;\n Addresses?: { AddressType?: string | null; AddressLine1?: string | null; AddressLine2?: string | null;\n AddressLine3?: string | null; AddressLine4?: string | null; City?: string | null;\n Region?: string | null; PostalCode?: string | null; Country?: string | null }[];\n}\ninterface XeroInvoice {\n InvoiceID: string; Type: \"ACCREC\" | \"ACCPAY\"; Status: string;\n InvoiceNumber?: string | null; Reference?: string | null;\n Contact?: { ContactID: string; Name?: string | null };\n Date?: string | null; DateString?: string | null; DueDate?: string | null; DueDateString?: string | null;\n CurrencyCode?: string | null; LineAmountTypes?: string | null; SubTotal?: number | null;\n TotalTax?: number | null; Total?: number | null; AmountDue?: number | null;\n AmountPaid?: number | null; AmountCredited?: number | null; FullyPaidOnDate?: string | null;\n UpdatedDateUTC?: string | null; SentToContact?: boolean; HasAttachments?: boolean;\n LineItems?: { LineItemID?: string; Description?: string | null; Quantity?: number | null;\n UnitAmount?: number | null; LineAmount?: number | null; TaxAmount?: number | null;\n TaxType?: string | null; AccountCode?: string | null; AccountID?: string | null;\n ItemCode?: string | null; DiscountRate?: number | null;\n Tracking?: { Name?: string | null; Option?: string | null }[] }[];\n}\ninterface XeroAttachment {\n AttachmentID: string; FileName: string; MimeType?: string | null; ContentLength?: number; IncludeOnline?: boolean;\n}\ninterface XeroConnection {\n id: string; tenantId: string; tenantType: string; tenantName?: string | null;\n createdDateUtc?: string | null; updatedDateUtc?: string | null;\n}\n",
        "requiredCredentials": [
          "xero-oauth"
        ],
        "metadata": {
          "category": "accounting",
          "readonly": true,
          "tags": [
            "readonly",
            "invoice-readiness"
          ]
        }
      },
      {
        "name": "listContacts",
        "description": "Read one page of customer/supplier contact and billing identity data.",
        "parameters": [
          {
            "name": "page",
            "type": "number",
            "optional": true,
            "description": "1..10000; one page is fetched per call."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true,
            "description": "1..100; defaults to 50."
          }
        ],
        "returnType": "Promise<{ contacts: XeroContact[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean }>",
        "typeDeclarations": "\ninterface XeroContact {\n ContactID: string; Name: string; ContactStatus?: string | null;\n AccountNumber?: string | null; ContactNumber?: string | null; FirstName?: string | null;\n LastName?: string | null; EmailAddress?: string | null; IsSupplier?: boolean;\n IsCustomer?: boolean; DefaultCurrency?: string | null; TaxNumber?: string | null;\n Addresses?: { AddressType?: string | null; AddressLine1?: string | null; AddressLine2?: string | null;\n AddressLine3?: string | null; AddressLine4?: string | null; City?: string | null;\n Region?: string | null; PostalCode?: string | null; Country?: string | null }[];\n}\ninterface XeroInvoice {\n InvoiceID: string; Type: \"ACCREC\" | \"ACCPAY\"; Status: string;\n InvoiceNumber?: string | null; Reference?: string | null;\n Contact?: { ContactID: string; Name?: string | null };\n Date?: string | null; DateString?: string | null; DueDate?: string | null; DueDateString?: string | null;\n CurrencyCode?: string | null; LineAmountTypes?: string | null; SubTotal?: number | null;\n TotalTax?: number | null; Total?: number | null; AmountDue?: number | null;\n AmountPaid?: number | null; AmountCredited?: number | null; FullyPaidOnDate?: string | null;\n UpdatedDateUTC?: string | null; SentToContact?: boolean; HasAttachments?: boolean;\n LineItems?: { LineItemID?: string; Description?: string | null; Quantity?: number | null;\n UnitAmount?: number | null; LineAmount?: number | null; TaxAmount?: number | null;\n TaxType?: string | null; AccountCode?: string | null; AccountID?: string | null;\n ItemCode?: string | null; DiscountRate?: number | null;\n Tracking?: { Name?: string | null; Option?: string | null }[] }[];\n}\ninterface XeroAttachment {\n AttachmentID: string; FileName: string; MimeType?: string | null; ContentLength?: number; IncludeOnline?: boolean;\n}\ninterface XeroConnection {\n id: string; tenantId: string; tenantType: string; tenantName?: string | null;\n createdDateUtc?: string | null; updatedDateUtc?: string | null;\n}\n",
        "requiredCredentials": [
          "xero-oauth"
        ],
        "metadata": {
          "category": "accounting",
          "readonly": true,
          "tags": [
            "readonly",
            "invoice-readiness"
          ]
        }
      },
      {
        "name": "getContact",
        "description": "Read one contact's billing identity and addresses.",
        "parameters": [
          {
            "name": "contactId",
            "type": "string"
          }
        ],
        "returnType": "Promise<XeroContact>",
        "typeDeclarations": "\ninterface XeroContact {\n ContactID: string; Name: string; ContactStatus?: string | null;\n AccountNumber?: string | null; ContactNumber?: string | null; FirstName?: string | null;\n LastName?: string | null; EmailAddress?: string | null; IsSupplier?: boolean;\n IsCustomer?: boolean; DefaultCurrency?: string | null; TaxNumber?: string | null;\n Addresses?: { AddressType?: string | null; AddressLine1?: string | null; AddressLine2?: string | null;\n AddressLine3?: string | null; AddressLine4?: string | null; City?: string | null;\n Region?: string | null; PostalCode?: string | null; Country?: string | null }[];\n}\ninterface XeroInvoice {\n InvoiceID: string; Type: \"ACCREC\" | \"ACCPAY\"; Status: string;\n InvoiceNumber?: string | null; Reference?: string | null;\n Contact?: { ContactID: string; Name?: string | null };\n Date?: string | null; DateString?: string | null; DueDate?: string | null; DueDateString?: string | null;\n CurrencyCode?: string | null; LineAmountTypes?: string | null; SubTotal?: number | null;\n TotalTax?: number | null; Total?: number | null; AmountDue?: number | null;\n AmountPaid?: number | null; AmountCredited?: number | null; FullyPaidOnDate?: string | null;\n UpdatedDateUTC?: string | null; SentToContact?: boolean; HasAttachments?: boolean;\n LineItems?: { LineItemID?: string; Description?: string | null; Quantity?: number | null;\n UnitAmount?: number | null; LineAmount?: number | null; TaxAmount?: number | null;\n TaxType?: string | null; AccountCode?: string | null; AccountID?: string | null;\n ItemCode?: string | null; DiscountRate?: number | null;\n Tracking?: { Name?: string | null; Option?: string | null }[] }[];\n}\ninterface XeroAttachment {\n AttachmentID: string; FileName: string; MimeType?: string | null; ContentLength?: number; IncludeOnline?: boolean;\n}\ninterface XeroConnection {\n id: string; tenantId: string; tenantType: string; tenantName?: string | null;\n createdDateUtc?: string | null; updatedDateUtc?: string | null;\n}\n",
        "requiredCredentials": [
          "xero-oauth"
        ],
        "metadata": {
          "category": "accounting",
          "readonly": true,
          "tags": [
            "readonly",
            "invoice-readiness"
          ]
        }
      },
      {
        "name": "listInvoiceAttachments",
        "description": "Read attachment metadata for one invoice. Files and provider URLs are not downloaded or exposed.",
        "parameters": [
          {
            "name": "invoiceId",
            "type": "string"
          }
        ],
        "returnType": "Promise<{ invoiceId: string; attachments: XeroAttachment[] }>",
        "typeDeclarations": "\ninterface XeroContact {\n ContactID: string; Name: string; ContactStatus?: string | null;\n AccountNumber?: string | null; ContactNumber?: string | null; FirstName?: string | null;\n LastName?: string | null; EmailAddress?: string | null; IsSupplier?: boolean;\n IsCustomer?: boolean; DefaultCurrency?: string | null; TaxNumber?: string | null;\n Addresses?: { AddressType?: string | null; AddressLine1?: string | null; AddressLine2?: string | null;\n AddressLine3?: string | null; AddressLine4?: string | null; City?: string | null;\n Region?: string | null; PostalCode?: string | null; Country?: string | null }[];\n}\ninterface XeroInvoice {\n InvoiceID: string; Type: \"ACCREC\" | \"ACCPAY\"; Status: string;\n InvoiceNumber?: string | null; Reference?: string | null;\n Contact?: { ContactID: string; Name?: string | null };\n Date?: string | null; DateString?: string | null; DueDate?: string | null; DueDateString?: string | null;\n CurrencyCode?: string | null; LineAmountTypes?: string | null; SubTotal?: number | null;\n TotalTax?: number | null; Total?: number | null; AmountDue?: number | null;\n AmountPaid?: number | null; AmountCredited?: number | null; FullyPaidOnDate?: string | null;\n UpdatedDateUTC?: string | null; SentToContact?: boolean; HasAttachments?: boolean;\n LineItems?: { LineItemID?: string; Description?: string | null; Quantity?: number | null;\n UnitAmount?: number | null; LineAmount?: number | null; TaxAmount?: number | null;\n TaxType?: string | null; AccountCode?: string | null; AccountID?: string | null;\n ItemCode?: string | null; DiscountRate?: number | null;\n Tracking?: { Name?: string | null; Option?: string | null }[] }[];\n}\ninterface XeroAttachment {\n AttachmentID: string; FileName: string; MimeType?: string | null; ContentLength?: number; IncludeOnline?: boolean;\n}\ninterface XeroConnection {\n id: string; tenantId: string; tenantType: string; tenantName?: string | null;\n createdDateUtc?: string | null; updatedDateUtc?: string | null;\n}\n",
        "requiredCredentials": [
          "xero-oauth"
        ],
        "metadata": {
          "category": "accounting",
          "readonly": true,
          "tags": [
            "readonly",
            "invoice-readiness"
          ]
        }
      }
    ]
  },
  {
    "slug": "twitter",
    "version": "1.0.3",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/twitter/1.0.3/d50fda212c63772a49b7cf3e7330384707427864198601f7176017fe8e436b3d.mjs",
    "digest": "d50fda212c63772a49b7cf3e7330384707427864198601f7176017fe8e436b3d",
    "contract_version": 1,
    "display_name": "X",
    "description": "Twitter/X posting and analytics: tweets, threads, engagement metrics",
    "category": "Social",
    "auth": {
      "type": "oauth2",
      "provider": "twitter-oauth",
      "label": "X (Twitter) OAuth",
      "hint": "Create an OAuth 2.0 app at developer.twitter.com; scopes cover tweet read/write, users.read, and offline.access.",
      "scopes": [
        "tweet.read",
        "tweet.write",
        "users.read",
        "offline.access"
      ],
      "callbackPath": "/callback"
    },
    "marketplace": {
      "displayName": "X",
      "category": "Social",
      "tagline": "Post to X (Twitter) and track engagement from agent workflows — tweets, threads, and analytics on a schedule.",
      "tags": [
        "social",
        "posting",
        "analytics"
      ],
      "useCases": [
        "Draft and post tweets on a schedule",
        "Post a thread from long-form content",
        "Track engagement metrics across posts",
        "Reply or quote-tweet from a queue"
      ],
      "permissionsSummary": "Post, delete, and read analytics for tweets via X OAuth. Posting and deleting are public, mutating actions; deletes are irreversible.",
      "links": {
        "homepage": "https://x.com",
        "docs": "https://developer.x.com/en/docs",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-twitter"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "dd841eb80c09f460bb7445e1dcc6df6e9f8fae9f",
      "notices_url": "https://orgsdk.ai/plugins/packages/twitter/1.0.3/d50fda212c63772a49b7cf3e7330384707427864198601f7176017fe8e436b3d.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# X (Twitter)\n\nPost to X — tweets, threads, and engagement analytics — and read account info.\nOAuth2 (BYO client).\n\n## What it does\n\nThe `twitter` plugin publishes tweets and threads, deletes tweets, fetches\nengagement analytics, and reads the authenticated user's profile. It targets\nthe X API v2 write/read scopes.\n\nTypical uses:\n\n- **Cross-posting** — publish a message across X, LinkedIn, and Facebook from\n  one workflow (compose with the `linkedin` and `facebook` plugins).\n- **Thread scheduling** — queue a multi-tweet thread for a launch.\n- **Performance review** — pull per-tweet analytics into a sheet.\n\n## Functions\n\n| Function | Purpose |\n|---|---|\n| `getUser` | Get the authenticated user's profile. |\n| `createTweet` | Post a single tweet (optionally a reply or quote). |\n| `createThread` | Post a thread (array of tweets). |\n| `deleteTweet` | Delete a tweet. |\n| `getTweetAnalytics` | Pull engagement metrics for a tweet. |\n\nThe full function list (parameters, return types) is available via the\n`plugins:info` command in the dashboard.\n\n## Credentials\n\nEvery function declares `requiredCredentials: [\"twitter-oauth\"]`. An X OAuth\ncredential must be connected before any function is callable.\n\n| Provider | Type | Connect method |\n|---|---|---|\n| `twitter-oauth` | oauth2 | BYO X (Twitter) OAuth 2.0 app. The granted scopes cover tweet read/write, users read, and offline access. |\n\nRequired scopes: `tweet.read`, `tweet.write`, `users.read`, `offline.access`.\n\n## Using it in a workflow\n\n```typescript\nconst me = await getUser();\n\nconst tweet = await createTweet({ text: \"We just shipped agent goals 🎯\" });\n\n// A reply in the same thread\nawait createTweet({\n  text: \"Thread 🧵 1/3 — what's new…\",\n  replyToId: tweet.id,\n});\n\n// Or post a whole thread at once\nawait createThread({\n  tweets: [\n    \"Three things we learned shipping agent iteration tracking:\",\n    \"1. Agents need durable goals, not just prompts.\",\n    \"2. Confidence scoring prevents noise from masking real wins.\",\n  ],\n});\n```\n\n## Notes\n\n- `createThread` posts sequentially and wires the reply chain for you.\n- `createTweet` accepts `replyToId` (reply) and `quoteTweetId` (quote) to\n  position the tweet in a conversation.\n- No `guides/` reference yet; agent usage patterns can be added later.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "getUser",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Get the authenticated user's Twitter/X profile including ID, name, and username.",
        "returnType": "Promise<{ id: string; name: string; username: string; profileImageUrl?: string }>",
        "parameters": [],
        "metadata": {
          "category": "twitter",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve the authenticated Twitter/X user's profile. Returns ID, name, username, and profile image URL.",
          "parameters": [],
          "returns": {
            "type": "Promise<TwitterUser>",
            "description": "The authenticated user's Twitter profile."
          },
          "examples": [
            {
              "title": "Get current user",
              "code": "const user = await twitter.getUser();\nconsole.log(`Posting as @${user.username}`);"
            }
          ]
        }
      },
      {
        "name": "createTweet",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Post a tweet. Supports replies and quote tweets. For threads, chain multiple tweets using replyToId.",
        "returnType": "Promise<{ id: string; url: string }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "replyToId",
            "type": "string",
            "optional": true
          },
          {
            "name": "quoteTweetId",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Post a tweet on Twitter/X. Supports replies and quote tweets. To create a thread, use the returned ID as replyToId for the next tweet.",
          "parameters": [
            {
              "name": "text",
              "type": "string",
              "description": "The tweet text. Max 280 characters for standard accounts, 4000 for premium.",
              "example": "\"Check out our new blog post on AI research!\""
            },
            {
              "name": "replyToId",
              "type": "string",
              "description": "Tweet ID to reply to. Use this to create threads by chaining tweets.",
              "optional": true
            },
            {
              "name": "quoteTweetId",
              "type": "string",
              "description": "Tweet ID to quote tweet.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ id: string; url: string }>",
            "description": "The created tweet ID and URL."
          },
          "examples": [
            {
              "title": "Post a simple tweet",
              "code": "const tweet = await twitter.createTweet({ text: \"Hello from Quokly!\" });"
            },
            {
              "title": "Create a thread",
              "code": "const t1 = await twitter.createTweet({ text: \"🧵 Thread: Why source transparency matters in AI research\" });\nconst t2 = await twitter.createTweet({ text: \"1/ Most AI tools give you answers without showing their work. That's a problem.\", replyToId: t1.id });\nconst t3 = await twitter.createTweet({ text: \"2/ Quokly shows you every source, classifies trust, and gives confidence scores.\", replyToId: t2.id });"
            }
          ]
        }
      },
      {
        "name": "createThread",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Post a Twitter/X thread (chain of tweets). Each tweet is posted as a reply to the previous one.",
        "returnType": "Promise<Array<{ id: string; url: string }>>",
        "parameters": [
          {
            "name": "tweets",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Post a complete Twitter/X thread in one call. Pass an array of tweet texts; they are automatically chained as replies.",
          "parameters": [
            {
              "name": "tweets",
              "type": "string[]",
              "description": "Array of tweet texts to post as a thread.",
              "example": "[\"Thread intro 🧵\", \"Point 1...\", \"Point 2...\", \"Conclusion + link\"]"
            }
          ],
          "returns": {
            "type": "Promise<Array<{ id: string; url: string }>>",
            "description": "Array of created tweet IDs and URLs."
          },
          "examples": [
            {
              "title": "Post a research insight thread",
              "code": "const thread = await twitter.createThread({\n  tweets: [\n    \"🧵 3 things I learned building an AI research tool for Australian startups\",\n    \"1/ Most founders don't verify AI sources. They trust the output blindly.\",\n    \"2/ Confidence scoring changes how you use AI. When the model says '45% confident', you double-check.\",\n    \"3/ Source classification (peer-reviewed vs blog vs social) is table stakes.\",\n    \"We built Quokly to solve all three → https://quokly.com.au\"\n  ]\n});"
            }
          ]
        }
      },
      {
        "name": "getTweetAnalytics",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Get analytics for a single tweet including impressions, likes, replies, retweets, quotes, bookmarks, and engagement rate.",
        "returnType": "Promise<{ impressions: number; likes: number; replies: number; retweets: number; quotes: number; bookmarks: number; engagementRate: number }>",
        "parameters": [
          {
            "name": "tweetId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "readonly",
            "analytics"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve engagement analytics for a single tweet by ID. Returns impressions, likes, replies, retweets, quotes, bookmarks, and calculated engagement rate.",
          "parameters": [
            {
              "name": "tweetId",
              "type": "string",
              "description": "The ID of the tweet to analyze."
            }
          ],
          "returns": {
            "type": "Promise<TwitterTweetAnalytics>",
            "description": "Engagement metrics for the tweet."
          }
        }
      },
      {
        "name": "deleteTweet",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Delete a tweet by ID. This action is irreversible.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "tweetId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "write",
            "destructive"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Delete a tweet. This action is irreversible.",
          "parameters": [
            {
              "name": "tweetId",
              "type": "string",
              "description": "The ID of the tweet to delete."
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean }>",
            "description": "Confirmation of deletion."
          }
        }
      }
    ]
  },
  {
    "slug": "shopify",
    "version": "0.1.1",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/shopify/0.1.1/297ed5b1852550460ac74118dc652eb3585071a3b658f86a9f6e98882ed422c2.mjs",
    "digest": "297ed5b1852550460ac74118dc652eb3585071a3b658f86a9f6e98882ed422c2",
    "contract_version": 1,
    "display_name": "Shopify",
    "description": "Read-only Shopify evidence for ecommerce late-order and return reviews.",
    "category": "Business",
    "auth": {
      "type": "token",
      "provider": "shopify",
      "label": "Shopify credentials",
      "hint": "Inject secret_accessToken. The operator maintains the scoped Admin API token externally."
    },
    "marketplace": {
      "displayName": "Shopify",
      "category": "Business",
      "tagline": "Read evidence for late orders and return recommendations.",
      "tags": [
        "ecommerce",
        "orders",
        "readonly",
        "exceptions"
      ],
      "useCases": [
        "Correlate order, shipment and support references",
        "Prepare evidence for a human-reviewed recommendation"
      ],
      "permissionsSummary": "Read operations only. No purchases, refunds, messages, updates or credential mutation. Account credentials may independently have broader rights.",
      "links": {
        "homepage": "https://orgsdk.ai",
        "docs": "https://shopify.dev/docs/api/admin-graphql/2026-10"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "12853e9545cf5bb12d41df5873055b519042cbdf",
      "notices_url": "https://orgsdk.ai/plugins/packages/shopify/0.1.1/297ed5b1852550460ac74118dc652eb3585071a3b658f86a9f6e98882ed422c2.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# OrgSDK Shopify evidence plugin\n\nRead-only Shopify Admin GraphQL API **2026-10**, pinned in source. Builds order,\nfulfillment and return-status evidence for late-order and return reviews. No\nmutations, refunds, order edits, customer notifications or arbitrary GraphQL.\n\n## Setup\n\n1. Create and install your own appropriate Shopify app with `read_orders` access.\n   Approve protected customer data access as required for that app's distribution.\n2. Obtain an Admin API access token using Shopify's supported authentication flow.\n   This plugin does not register an app, exchange client credentials or run OAuth.\n   Client ID/client secret and Storefront API tokens are not Admin access tokens.\n3. Configure `{ \"shop\": \"your-store.myshopify.com\" }` in the plugin. Only a single\n   lowercase DNS label plus `.myshopify.com` is accepted; no scheme, custom domain,\n   path, port or alternate host. Optional `timeoutMs` controls the bounded timeout.\n4. Attach provider `shopify` with secret key `secret_accessToken` through OrgSDK.\n   Keep token issuance/renewal outside this plugin and replace expired credentials\n   through OrgSDK's normal credential settings. Never put secrets in configuration.\n5. Call `testConnection({})`; it verifies the returned canonical shop domain.\n\nShopify's client-credentials grant is for an installed app on stores owned by your\norganization and currently issues tokens with an expiry (24 hours). Other merchant\ninstalls require the applicable app authentication/authorization flow and may use\nother expiring token types. Token lifetime comes from the grant response, not this\nplugin. Do not assume a permanent admin-created custom-app token is available.\n\n`read_orders` covers these Order/Fulfillment reads. Only the most recent 60 days\nare normally visible. Older records need approved `read_all_orders` access in\naddition to the required order read scope. No write scope is needed here.\nOrder information is protected customer data even when names, phone, email and\naddresses are excluded, as here. Public app review/entitlements may still be\nrequired; redacted fields or access errors are not proof that no orders exist.\nA successful shop check does not prove order-data or protected-data entitlement.\n\n## Named functions\n\n| Function               | Arguments                     | Result                                                        |\n| ---------------------- | ----------------------------- | ------------------------------------------------------------- |\n| `testConnection`       | `{}`                          | shop ID, name, canonical domain                               |\n| `listOrders`           | `{pageSize?, after?, query?}` | nodes and `pageInfo`                                          |\n| `getOrder`             | `{id}`                        | exact order summary                                           |\n| `listOrderLineItems`   | `{id, pageSize?, after?}`     | order ID/name and paginated SKU/quantity evidence             |\n| `getOrderFulfillments` | `{id, pageSize?}`             | order ID/name, fulfillment count and bounded tracking samples |\n\n`id` is an exact `gid://shopify/Order/<digits>` obtained from this store. The\nfixed configured store and its token determine account access; GIDs alone do not\nencode store ownership. `pageSize` is 1–100 (default 25), `after` is a bounded\nopaque cursor. `query` is at most 512 characters of Shopify **order search** syntax\n(e.g. `status:open fulfillment_status:unfulfilled`), supplied only as a GraphQL\nvariable. There is no query-document override. Order pages sort by creation time.\n\nOrder summaries include ID/name, dates, financial/fulfillment/return statuses and\nshop-currency total. Line items include ID/name, SKU, quantity, current quantity\nand refundable quantity. These quantities are evidence, not refund approval.\nContinue order and line-item pages using `pageInfo.endCursor` while\n`hasNextPage`. Fulfillments are an API array with a `first` cap; its count and\nprecision expose possible truncation. Tracking is at most 20 references per\nfulfillment and must always be treated as a sample. There is no fulfillment or\ntracking cursor in this API field. API/schema or scope changes fail closed as\n`GRAPHQL_ERROR`, without returning partial data or upstream error text.\n\n## Official references\n\n- [GraphQL API authentication and endpoint](https://shopify.dev/docs/api/admin-graphql/2026-10)\n- [Orders and search](https://shopify.dev/docs/api/admin-graphql/2026-10/queries/orders)\n- [Order fields, scope and 60-day boundary](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/Order)\n- [Fulfillment fields and tracking caps](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/Fulfillment)\n- [Client credentials grant and token expiry](https://shopify.dev/docs/apps/build/authentication-authorization/client-credentials-grant)\n- [Protected customer data](https://shopify.dev/docs/apps/launch/protected-customer-data)\n\nPrivate read-only integration. See the repository README for verification and artifact instructions.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "shop": {
          "type": "string",
          "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.myshopify\\.com$"
        },
        "timeoutMs": {
          "default": 15000,
          "type": "integer",
          "minimum": 100,
          "maximum": 30000
        }
      },
      "required": [
        "shop"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "testConnection",
        "description": "Read shop identity and verify it matches the configured myshopify.com store.",
        "calling": "named",
        "parameters": [],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shopify"
        ],
        "metadata": {
          "readonly": true,
          "category": "shopify",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "listOrders",
        "description": "Read one cursor page of order evidence. Search uses Shopify order search syntax, never GraphQL.",
        "calling": "named",
        "parameters": [
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          },
          {
            "name": "after",
            "type": "string",
            "optional": true
          },
          {
            "name": "query",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shopify"
        ],
        "metadata": {
          "readonly": true,
          "category": "shopify",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "getOrder",
        "description": "Read one exact order, including return and fulfillment status.",
        "calling": "named",
        "parameters": [
          {
            "name": "id",
            "type": "string",
            "description": "Exact resource ID from this account."
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shopify"
        ],
        "metadata": {
          "readonly": true,
          "category": "shopify",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "listOrderLineItems",
        "description": "Read one cursor page of purchased SKU and quantity evidence for a return review.",
        "calling": "named",
        "parameters": [
          {
            "name": "id",
            "type": "string",
            "description": "Exact resource ID from this account."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          },
          {
            "name": "after",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shopify"
        ],
        "metadata": {
          "readonly": true,
          "category": "shopify",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "getOrderFulfillments",
        "description": "Read up to 100 fulfillments with up to 20 tracking references each. Count exposes fulfillment truncation; tracking is always a bounded sample.",
        "calling": "named",
        "parameters": [
          {
            "name": "id",
            "type": "string",
            "description": "Exact resource ID from this account."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shopify"
        ],
        "metadata": {
          "readonly": true,
          "category": "shopify",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      }
    ]
  },
  {
    "slug": "shippo",
    "version": "0.1.1",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/shippo/0.1.1/878fdd3ec72a3c0b608129bc5f921b59341c09e8dffcf958798c67c264faffa0.mjs",
    "digest": "878fdd3ec72a3c0b608129bc5f921b59341c09e8dffcf958798c67c264faffa0",
    "contract_version": 1,
    "display_name": "Shippo",
    "description": "Read-only Shippo evidence for ecommerce late-order and return reviews.",
    "category": "Business",
    "auth": {
      "type": "token",
      "provider": "shippo",
      "label": "Shippo credentials",
      "hint": "Inject secret_apiKey. Uses ShippoToken authentication."
    },
    "marketplace": {
      "displayName": "Shippo",
      "category": "Business",
      "tagline": "Read evidence for late orders and return recommendations.",
      "tags": [
        "ecommerce",
        "shipping",
        "readonly",
        "exceptions"
      ],
      "useCases": [
        "Correlate order, shipment and support references",
        "Prepare evidence for a human-reviewed recommendation"
      ],
      "permissionsSummary": "Read operations only. No purchases, refunds, messages, updates or credential mutation. Account credentials may independently have broader rights.",
      "links": {
        "homepage": "https://orgsdk.ai",
        "docs": "https://docs.goshippo.com/api-reference/overview"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "1151d89f3450607cea74afccd794bc3c4e53010f",
      "notices_url": "https://orgsdk.ai/plugins/packages/shippo/0.1.1/878fdd3ec72a3c0b608129bc5f921b59341c09e8dffcf958798c67c264faffa0.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# OrgSDK Shippo evidence plugin\n\nRead-only existing Shippo transactions, shipment references and tracking evidence.\nUses `https://api.goshippo.com` with the currently documented **2018-02-08**\nAPI version header. No label purchases, rate shopping, refunds, shipment creation,\ntracking registration or webhook changes are exposed.\n\n## Setup\n\n1. In your own Shippo account's API portal, open API Configuration > Developer keys\n   and generate an appropriate test or live key. Use test credentials initially.\n2. Attach provider `shippo` in OrgSDK with secret key `secret_apiKey`. The plugin\n   sends `Authorization: ShippoToken <key>` and `SHIPPO-API-VERSION: 2018-02-08`.\n3. Configuration is `{}` or `{ \"timeoutMs\": 15000 }`. Account IDs, hosts and\n   credential overrides are intentionally rejected. Managed-account impersonation\n   via `SHIPPO-ACCOUNT-ID` is not supported; use a key for the intended account.\n4. Call `testConnection({})`. This makes GET `/transactions/?results=1` and proves\n   transaction read access only. Shippo's documented API does not offer a suitable\n   general current-user identity read here; `identityVerified` is false.\n\nShippo documents account API keys, not a granular read-only key scope for these\nendpoints. Use whatever account/key restrictions Shippo actually provides; this\nadapter's read-only surface is not a claim that the underlying key cannot buy\nlabels elsewhere. The injected key determines account and test/live boundary;\ncallers cannot override either. Test and live datasets are separate. The plugin\ndoes not rotate credentials, refresh tokens or validate billing entitlements.\n\n## Named functions\n\n| Function           | Arguments                   | Result                                                                |\n| ------------------ | --------------------------- | --------------------------------------------------------------------- |\n| `testConnection`   | `{}`                        | connection/read access, version, no identity claim                    |\n| `listTransactions` | `{page?, pageSize?}`        | projected transactions, count and `nextPage`                          |\n| `getTransaction`   | `{id}`                      | exact existing transaction references                                 |\n| `listShipments`    | `{page?, pageSize?}`        | existing shipment correlation references                              |\n| `getShipment`      | `{id}`                      | existing shipment correlation references                              |\n| `getTracking`      | `{carrier, trackingNumber}` | matching carrier/number, transaction, ETA, status and bounded history |\n\nResource IDs are exactly 32 lowercase hexadecimal characters; `page` is 1–10,000 and\n`pageSize` is 1–100 (default 25). Continue by the returned numeric `nextPage`.\nThe server's `next` URL is never fetched or returned. `hasMore` reports source\ncontinuation; `truncated` is true at the 10,000-page safety ceiling, where\n`nextPage` is null. No automatic page traversal.\n\nTransactions retain object IDs/timestamps, status, metadata, order, parcel,\nrate/shipment/carrier references and tracking references. Signed label URLs are\nexcluded. Shipments return identity, timestamps, status, metadata and order\nreferences, without recipient addresses, parcels or rate collections. Correlate\nstored order references/metadata against Shopify explicitly: these are optional,\nmerchant-provided fields, not guaranteed cross-service foreign keys.\n\n`carrier` is a documented Shippo carrier token (e.g. `usps`), lowercase letters,\ndigits or underscore, at most 64 characters. Tracking numbers accept only letters,\ndigits, hyphen and underscore, at most 128 characters. Unsupported formats fail\nvalidation; do not rewrite a carrier's number to force acceptance. GET tracking\nmust return the exact requested carrier and number. It retains the latest 100\nhistory entries as returned by Shippo and sets `historyTruncated`; location data\nis excluded. History is evidence of carrier reports, not guaranteed current\nphysical location. GET tracking does not register a webhook. Existing shipment\nretention and carrier availability limit historical evidence (Shippo documents\n390-day retrieval limits for shipment objects). API use may be metered by Shippo.\n\n## Official references\n\n- [API overview and version](https://docs.goshippo.com/api-reference/overview)\n- [Authentication and key limits](https://docs.goshippo.com/guides/authentication)\n- [List existing transactions](https://docs.goshippo.com/api-reference/transactions/list-all-shipping-labels)\n- [Retrieve existing transactions](https://docs.goshippo.com/api-reference/transactions/retrieve-a-shipping-label)\n- [List shipments](https://docs.goshippo.com/api-reference/shipments/list-all-shipments)\n- [Retrieve a shipment and retention](https://docs.goshippo.com/api-reference/shipments/retrieve-a-shipment)\n- [GET tracking status](https://docs.goshippo.com/api-reference/tracking-status/get-a-tracking-status)\n\nPrivate read-only integration. See the repository README for verification and artifact instructions.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "timeoutMs": {
          "default": 15000,
          "type": "integer",
          "minimum": 100,
          "maximum": 30000
        }
      },
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "testConnection",
        "description": "Verify transaction read access with a one-record request. Does not establish an account owner identity.",
        "calling": "named",
        "parameters": [],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shippo"
        ],
        "metadata": {
          "readonly": true,
          "category": "shippo",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "listTransactions",
        "description": "Read one numbered page of existing shipping-label transactions; never purchase a label.",
        "calling": "named",
        "parameters": [
          {
            "name": "page",
            "type": "number",
            "optional": true
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shippo"
        ],
        "metadata": {
          "readonly": true,
          "category": "shippo",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "getTransaction",
        "description": "Read an exact transaction with order, rate and tracking references. Excludes label download URLs.",
        "calling": "named",
        "parameters": [
          {
            "name": "id",
            "type": "string",
            "description": "Exact resource ID from this account."
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shippo"
        ],
        "metadata": {
          "readonly": true,
          "category": "shippo",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "listShipments",
        "description": "Read one page of existing shipment references; no addresses, parcel creation or rate shopping.",
        "calling": "named",
        "parameters": [
          {
            "name": "page",
            "type": "number",
            "optional": true
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shippo"
        ],
        "metadata": {
          "readonly": true,
          "category": "shippo",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "getShipment",
        "description": "Read an existing shipment's correlation references, excluding recipient addresses and rates.",
        "calling": "named",
        "parameters": [
          {
            "name": "id",
            "type": "string",
            "description": "Exact resource ID from this account."
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shippo"
        ],
        "metadata": {
          "readonly": true,
          "category": "shippo",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      },
      {
        "name": "getTracking",
        "description": "GET tracking evidence for an exact carrier token and tracking number. Does not register tracking webhooks.",
        "calling": "named",
        "parameters": [
          {
            "name": "carrier",
            "type": "string"
          },
          {
            "name": "trackingNumber",
            "type": "string"
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "shippo"
        ],
        "metadata": {
          "readonly": true,
          "category": "shippo",
          "tags": [
            "readonly",
            "ecommerce"
          ]
        }
      }
    ]
  },
  {
    "slug": "servicem8",
    "version": "0.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/servicem8/0.1.2/0a4ce682d51c0d61792e738b65a4b38c9627d460541c7bb129e392d84d6b12dd.mjs",
    "digest": "0a4ce682d51c0d61792e738b65a4b38c9627d460541c7bb129e392d84d6b12dd",
    "contract_version": 1,
    "display_name": "Servicem8",
    "description": "Read-only bounded servicem8 workflow evidence for OrgSDK.",
    "category": "Productivity",
    "auth": {
      "type": "token",
      "provider": "servicem8",
      "label": "servicem8 credential",
      "hint": "Injected secrets only; see README for setup and permission boundaries."
    },
    "marketplace": {
      "displayName": "Servicem8",
      "category": "Productivity",
      "permissionsSummary": "Read-only evidence retrieval, constrained by mandatory configured resource boundaries. No external writes.",
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "002d3d2b856347eeaf7743289edb8756cf8d194f",
      "notices_url": "https://orgsdk.ai/plugins/packages/servicem8/0.1.2/0a4ce682d51c0d61792e738b65a4b38c9627d460541c7bb129e392d84d6b12dd.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# ServiceM8 evidence plugin\n\n## Current npm distribution and execution checks\n\nUpdated **31 October 2026**. This public-release candidate uses `@orgsdk/plugins@0.2.2`\nand `@orgsdk/executor@0.1.0` (with the tracked packaging compatibility patch).\n`bun run build:npm` creates a public Apache-2.0 npm payload; `bun run test:executor`\ndownloads the extracted npm entry into an empty executor store and executes every exported function headlessly.\n`bun run test:registration` verifies fresh-project registration using the npm host.\nSee [npm distribution, CI activation and verification](docs/npm-distribution.md)\nfor prerequisites, commands, fixtures and release settings. Publication remains\nan explicit release operation; source-root `private: true` is retained.\n\nReview completed jobs, recorded work, invoice state, client details and job attachment metadata before an operator prepares invoices. Returned status and documents are evidence; they do not establish an approval or create an invoice.\n\n## Install in OrgSDK\n\nOpen your project's Plugins page, select this plugin and enable the pinned release.\nThe default catalogue is https://orgsdk.ai/plugins/releases.json. The executor\ndownloads the versioned bundle and verifies its SHA256. Configure the provider\nand attach the credential described below before running an action.\n\nThe plugin is Apache-2.0 licensed; see LICENSE, NOTICE and TRADEMARKS.md.\nThe generated npm distribution is a separate publication surface. Installing npm\nfiles alone does not register a project plugin. Local builds do not publish it.\n\n## Setup\n\nFor a private integration with your own account or one specific customer's account, create a ServiceM8 API key under Settings > API Keys. Prefer a read-only key. Store it in an OrgSDK `servicem8` credential as `secret_apiKey`. ServiceM8 officially supports `X-API-Key` authentication for private integrations. Their docs distinguish private applications from broadly distributed/add-on-directory applications, which use OAuth; this plugin implements only the private API-key path. Review this distinction before wider distribution.\n\nConfigure `{ \"accountUuid\": \"your-account-uuid\" }`. This expected account is mandatory. Every operation first requests `/addonsdk/whoami.json` and verifies `account.uuid`; no business resource is requested if the account differs or identity cannot be verified. `getAccount` returns account UUID/name and key access level, excluding email. This implementation has account-wide read visibility within the expected account, not a job/client allowlist. Use only the intended account's key. Keys are never read from configuration or environment variables.\n\n## Tools\n\n- `getAccount({})`\n- `listJobs({ status?: \"Completed\" | \"Quote\" | \"Work Order\" | \"Unsuccessful\", cursor?, modifiedSince?: \"YYYY-MM-DD\" })` defaults to Completed; filters active records and optional edit_date greater than the supplied date.\n- `getJob({ jobId })`\n- `listClients({ cursor? })`\n- `getClient({ clientId })`\n- `listJobAttachments({ jobId, cursor? })`\n- `getJobAttachment({ jobId, attachmentId })`\n\nLists use the documented ServiceM8 cursor protocol: default cursor `-1`, up to 1,000 records per response, and `{ records, nextCursor }` from the `x-next-cursor` header. Each call reads one bounded page with a 2 MB body cap; unusually large pages fail instead of truncating and silently skipping evidence. Explicitly follow `nextCursor` to continue. No automatic pagination or retry.\n\nJob evidence includes descriptions of requested/completed work, completion date, invoice date/sent state, total and payment/export observations when present. The deprecated `ready_to_invoice` flag is intentionally omitted. Job/client web links use ServiceM8's documented signed-in application URLs; these are navigation links, not anonymous invoice/download links. Invoice/quote attachment source labels are exposed through job-related metadata. Attachment reads verify `related_object=job` and matching `related_object_uuid` before returning anything. Raw attachment metadata, signatures, signed URLs and extracted content are not returned.\n\nBinary attachment downloads are intentionally unavailable in this initial release. The documented file endpoint returns a 302 to unspecified external storage hosts; this plugin denies redirects and cannot safely promise a constrained download host. It exposes no temporary storage URL. No invoices, jobs, notes, clients, messages or payments are created or changed.\n\n## Verified provider contract\n\nReviewed 2026-10-30 against official [API-key authentication](https://developer.servicem8.com/docs/authentication), [account identity](https://developer.servicem8.com/reference/getaddonsdkwhoami), [job reads](https://developer.servicem8.com/reference/getjobs), [client reads](https://developer.servicem8.com/reference/getclients), [attachment metadata](https://developer.servicem8.com/reference/getattachments), [attachment download redirect contract](https://developer.servicem8.com/reference/downloadattachmentfile), [filters](https://developer.servicem8.com/docs/filtering) and [cursor pagination](https://developer.servicem8.com/docs/pagination). API base: `https://api.servicem8.com/api_1.0/`. Official attachment detail path is `/dboattachment/{uuid}.json`; list uses `/attachment.json`. OAuth reference scopes are `read_jobs`, `read_customers`, `read_attachments`; this plugin does not implement OAuth.\n\n## Runtime and safety\n\nThis Apache-2.0 plugin targets the current local OrgSDK factory loader. Its only runtime dependency is Zod. The authoring SDK is dev/type-only; each repository owns its lockfile. Credentials arrive as `{ provider: secretRecord }` through `requiredCredentials`; there is no environment fallback. All tools accept one named argument object and perform GET requests only. Unknown arguments/configuration fail validation. There are no approval flags, mutation tools, HTTP callbacks, auth providers or token refresh handlers.\n\nEvery request uses a fixed HTTPS API origin, manual redirects (all 3xx fail), a 15-second abort deadline covering headers and body streaming, and a 2,000,000-byte metadata/JSON cap. Errors contain status codes or generic connection guidance, never upstream response bodies, request URLs or credential values. One explicit page is read per list call; no automatic retries or pagination loops occur. An HTTP 429 requires the caller to retry later. Returned evidence is untrusted provider content and is not an instruction or approval. Metadata timestamps and custom fields are observations, not a determination that work or payment is approved.\n\n## Local validation and release\n\nUse Bun 1.4 and Node.js 22.22.0 or newer. `bun install --frozen-lockfile`, then `bun run check`, `bun run typecheck`, `bun run test`, `bun run validate`, `bun run package`, and `bun run version-guard`. `dist/` is the authoring CLI's deterministic source package. The active local platform's `package-plugin` command creates the separately executable ESM archive in `release/`; the legacy source archive alone is not executable by the current runtime. The release includes this repository's Apache-2.0 LICENSE. No publication, remote push or live customer test is part of local verification.\n\nTests use injected fetch fixtures only. They cover credential enforcement, GET-only transport, redirects, generic errors, response-size limits, stream cancellation, provider boundaries, pagination and handler evidence. Live provider behavior and OAuth consent still require an operator-controlled integration test with an explicitly selected account.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "accountUuid": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
          "description": "Expected ServiceM8 account UUID; verified through whoami before every operation."
        }
      },
      "required": [
        "accountUuid"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "getAccount",
        "description": "Verify API key and expected account; return account identity and key access level.",
        "parameters": [],
        "returnType": "Promise<{ account: { uuid: string; name?: string }; key: { access_level: string; auth_method?: string } }>",
        "requiredCredentials": [
          "servicem8"
        ],
        "metadata": {
          "readonly": true,
          "category": "servicem8",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "listJobs",
        "description": "Read one ServiceM8 cursor page of active jobs; defaults to Completed for invoice evidence.",
        "parameters": [
          {
            "name": "status",
            "type": "\"Completed\" | \"Quote\" | \"Work Order\" | \"Unsuccessful\"",
            "optional": true
          },
          {
            "name": "cursor",
            "type": "string",
            "optional": true
          },
          {
            "name": "modifiedSince",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<{ records: Record<string, unknown>[]; nextCursor: string | null }>",
        "requiredCredentials": [
          "servicem8"
        ],
        "metadata": {
          "readonly": true,
          "category": "servicem8",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "getJob",
        "description": "Read job scope, work done, completion and invoice fields. Web link requires ServiceM8 sign-in.",
        "parameters": [
          {
            "name": "jobId",
            "type": "string"
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "servicem8"
        ],
        "metadata": {
          "readonly": true,
          "category": "servicem8",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "listClients",
        "description": "Read one cursor page of active clients in the verified account.",
        "parameters": [
          {
            "name": "cursor",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<{ records: Record<string, unknown>[]; nextCursor: string | null }>",
        "requiredCredentials": [
          "servicem8"
        ],
        "metadata": {
          "readonly": true,
          "category": "servicem8",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "getClient",
        "description": "Read selected client and billing metadata in the verified account.",
        "parameters": [
          {
            "name": "clientId",
            "type": "string"
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "servicem8"
        ],
        "metadata": {
          "readonly": true,
          "category": "servicem8",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "listJobAttachments",
        "description": "Read one page of job-related attachment metadata, including quote or invoice source labels.",
        "parameters": [
          {
            "name": "jobId",
            "type": "string"
          },
          {
            "name": "cursor",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<{ records: Record<string, unknown>[]; nextCursor: string | null }>",
        "requiredCredentials": [
          "servicem8"
        ],
        "metadata": {
          "readonly": true,
          "category": "servicem8",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "getJobAttachment",
        "description": "Read attachment metadata only after checking it belongs to the requested job.",
        "parameters": [
          {
            "name": "jobId",
            "type": "string"
          },
          {
            "name": "attachmentId",
            "type": "string"
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "servicem8"
        ],
        "metadata": {
          "readonly": true,
          "category": "servicem8",
          "tags": [
            "readonly"
          ]
        }
      }
    ]
  },
  {
    "slug": "serper",
    "version": "1.1.8",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/serper/1.1.8/e1410b65dd0baf56bf4e19f8f79bd008fe48fae0a0f2d6d79da4ef8027fa4c0b.mjs",
    "digest": "e1410b65dd0baf56bf4e19f8f79bd008fe48fae0a0f2d6d79da4ef8027fa4c0b",
    "contract_version": 1,
    "display_name": "Serper",
    "description": "Google Search via Serper.dev — organic results, news, images, knowledge graph, and answer boxes",
    "category": "Search & Research",
    "auth": {
      "type": "token",
      "provider": "serper",
      "label": "Serper.dev API key",
      "hint": "Create a key at serper.dev."
    },
    "marketplace": {
      "displayName": "Serper",
      "category": "Search & Research",
      "tagline": "Google Search for agents — organic results, news, images, knowledge graph, and full-page scraping for research workflows.",
      "tags": [
        "search",
        "research"
      ],
      "useCases": [
        "Research a topic and synthesize an answer with sources",
        "Monitor news and results for a keyword on a schedule",
        "Scrape pages to enrich a lead or competitor profile",
        "Find images and source links for content workflows"
      ],
      "permissionsSummary": "Read-only web search and page scraping via the Serper.dev API key. No write or destructive actions.",
      "links": {
        "homepage": "https://serper.dev",
        "docs": "https://serper.dev/playground",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-serper"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "aad0fd7c0c79eebed963beafc5981b92cd4d77a2",
      "notices_url": "https://orgsdk.ai/plugins/packages/serper/1.1.8/e1410b65dd0baf56bf4e19f8f79bd008fe48fae0a0f2d6d79da4ef8027fa4c0b.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Serper\n\nGoogle Search via Serper.dev — organic results, news, images, knowledge graph,\nanswer boxes, and live page scraping. Token auth (API key).\n\n## What it does\n\nThe `serper` plugin gives workflows live web search: run a Google query and get\nranked organic results, news, images, or the knowledge-graph/answer-box\nsummary for a query, plus the ability to scrape a URL's content. It is the\nprimary \"what's happening on the web right now\" input — distinct from the\n`llm` plugin's `querySearch`, which is knowledge-based (not live).\n\nTypical uses:\n\n- **Research** — gather fresh sources before summarising with the `llm` plugin.\n- **Monitoring** — watch a competitor's name or a keyword for new results on a\n  schedule.\n- **Enrichment** — scrape a company's homepage to enrich a prospect record.\n- **News roundups** — pull top news for a topic into a digest.\n\n## Functions\n\n| Function | Purpose |\n|---|---|\n| `search` | Organic Google results for a query. |\n| `newsSearch` | News results for a query. |\n| `imageSearch` | Image results for a query. |\n| `scrapeUrl` | Fetch and extract the content of a URL. |\n\nCommon optional params across search functions: `num` (result count), `gl`\n(country), `hl` (language), `location`, `timeRange`, `autocorrect`, `page`.\n\nThe full function list (parameters, return types) is available via the\n`plugins:info` command in the dashboard.\n\n## Credentials\n\nEvery function declares `requiredCredentials: [\"serper\"]`. A Serper.dev API key\nmust be connected before any function is callable.\n\n| Provider | Type | How to get it |\n|---|---|---|\n| `serper` | token | Create a key at `serper.dev` (free tier available). |\n\nConnect the token inline from the plugin row, or via the CLI:\n\n```bash\norgsdk credential set serper default your_serper_key\n```\n\n## Using it in a workflow\n\n```typescript\n// Fresh competitive research, then summarise\nconst results = await search({\n  q: \"AI agent orchestration platform\",\n  num: 10,\n  gl: \"us\",\n});\n// const summary = await synthesizeAnswer(results.organic.map(r => r.snippet)); // llm plugin\n\n// Scrape a specific page for deeper content\nconst page = await scrapeUrl({ url: \"https://acme.com/about\" });\n```\n\n## Notes\n\n- Prefer `search` for ranked links and `scrapeUrl` when you need the full\n  page body of a known URL.\n- `num`, `gl`, `hl`, `timeRange` meaningfully change result quality — set them\n  deliberately rather than relying on defaults.\n- No `guides/` reference yet; agent usage patterns can be added later.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "defaultCountry": {
          "description": "Two-letter country code for search results (e.g. \"us\", \"au\", \"uk\")",
          "type": "string",
          "minLength": 2,
          "maxLength": 2
        },
        "defaultLanguage": {
          "description": "Language code for results (e.g. \"en\", \"es\", \"fr\")",
          "type": "string"
        },
        "defaultNumResults": {
          "description": "Number of results per page (1–100)",
          "type": "integer",
          "minimum": 1,
          "maximum": 100
        },
        "defaultDevice": {
          "description": "Device type for search results",
          "type": "string",
          "enum": [
            "desktop",
            "mobile"
          ]
        }
      }
    },
    "tools": [
      {
        "name": "search",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Search Google via Serper.dev. Returns organic results, knowledge graph, answer box, people-also-ask, and related searches.",
        "returnType": "Promise<SearchResult>",
        "typeDeclarations": "\n\t\t\ttype TimeRange = \"past_hour\" | \"past_24h\" | \"past_week\" | \"past_month\" | \"past_year\";\n\t\t\tinterface OrganicResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\tdate?: string;\n\t\t\t\tsitelinks?: Array<{ title: string; link: string }>;\n\t\t\t}\n\t\t\tinterface AnswerBox {\n\t\t\t\tsnippet?: string;\n\t\t\t\ttitle?: string;\n\t\t\t\tlink?: string;\n\t\t\t\tanswer?: string;\n\t\t\t}\n\t\t\tinterface KnowledgeGraph {\n\t\t\t\ttitle?: string;\n\t\t\t\ttype?: string;\n\t\t\t\tdescription?: string;\n\t\t\t\tsourceUrl?: string;\n\t\t\t\tattributes?: Record<string, string>;\n\t\t\t}\n\t\t\tinterface SearchResult {\n\t\t\t\tquery: string;\n\t\t\t\tsearchParameters: { q: string; gl: string; hl?: string; page: number; tbs?: string; };\n\t\t\t\tanswerBox?: AnswerBox;\n\t\t\t\tknowledgeGraph?: KnowledgeGraph;\n\t\t\t\torganic: OrganicResult[];\n\t\t\t\tpeopleAlsoAsk?: Array<{ question: string; snippet?: string; link?: string }>;\n\t\t\t\trelatedSearches?: Array<{ query: string }>;\n\t\t\t}\n\t\t\tinterface ImageResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\timageUrl: string;\n\t\t\t\timageWidth?: number;\n\t\t\t\timageHeight?: number;\n\t\t\t\tsource?: string;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "q",
            "type": "string",
            "description": "The search query."
          },
          {
            "name": "gl",
            "type": "string",
            "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
            "optional": true
          },
          {
            "name": "hl",
            "type": "string",
            "description": "Language code (e.g. \"en\", \"es\").",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "description": "Result page number (1-based). Default: 1.",
            "optional": true
          },
          {
            "name": "num",
            "type": "number",
            "description": "Number of results per page. Default: 10.",
            "optional": true
          },
          {
            "name": "timeRange",
            "type": "TimeRange",
            "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
            "optional": true
          },
          {
            "name": "location",
            "type": "string",
            "description": "Location string (e.g. \"Austin,Texas,United States\").",
            "optional": true
          },
          {
            "name": "device",
            "type": "\"desktop\" | \"mobile\"",
            "description": "Device type for results. Default: desktop.",
            "optional": true
          },
          {
            "name": "autocorrect",
            "type": "boolean",
            "description": "Whether to apply autocorrect. Default: true.",
            "optional": true
          }
        ],
        "metadata": {
          "category": "search",
          "tags": [
            "readonly",
            "search",
            "google",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Search Google using the Serper.dev API. Returns rich results including organic listings, knowledge graph panels, answer boxes, related questions, and more.",
          "parameters": [
            {
              "name": "q",
              "type": "string",
              "description": "The search query.",
              "example": "\"apple inc\""
            },
            {
              "name": "gl",
              "type": "string",
              "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
              "optional": true,
              "defaultValue": "us"
            },
            {
              "name": "hl",
              "type": "string",
              "description": "Language code (e.g. \"en\", \"es\").",
              "optional": true
            },
            {
              "name": "page",
              "type": "number",
              "description": "Result page number (1-based). Default: 1.",
              "optional": true,
              "defaultValue": "1"
            },
            {
              "name": "num",
              "type": "number",
              "description": "Number of results per page. Default: 10.",
              "optional": true,
              "defaultValue": "10"
            },
            {
              "name": "timeRange",
              "type": "TimeRange",
              "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
              "optional": true
            },
            {
              "name": "location",
              "type": "string",
              "description": "Location string (e.g. \"Austin,Texas,United States\").",
              "optional": true
            },
            {
              "name": "device",
              "type": "\"desktop\" | \"mobile\"",
              "description": "Device type for results. Default: desktop.",
              "optional": true
            },
            {
              "name": "autocorrect",
              "type": "boolean",
              "description": "Whether to apply autocorrect. Default: true.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<SearchResult>",
            "description": "Search results including organic listings, knowledge graph, answer box, people-also-ask, and related searches."
          },
          "examples": [
            {
              "title": "Basic search",
              "code": "const results = await serper.search({ q: \"climate change impact\" });\nfor (const r of results.organic) {\n  console.log(`${r.position}. ${r.title}\\n   ${r.link}`);\n}"
            },
            {
              "title": "Search with time range and country",
              "code": "const recent = await serper.search({\n  q: \"AI news\",\n  gl: \"au\",\n  timeRange: \"past_24h\",\n  num: 5,\n});"
            },
            {
              "title": "Paginated search",
              "code": "const page2 = await serper.search({\n  q: \"typescript best practices\",\n  page: 2,\n  num: 20,\n});"
            }
          ]
        }
      },
      {
        "name": "newsSearch",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Search Google News via Serper.dev. Returns news articles matching the query.",
        "returnType": "Promise<SearchResult>",
        "typeDeclarations": "\n\t\t\ttype TimeRange = \"past_hour\" | \"past_24h\" | \"past_week\" | \"past_month\" | \"past_year\";\n\t\t\tinterface OrganicResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\tdate?: string;\n\t\t\t\tsitelinks?: Array<{ title: string; link: string }>;\n\t\t\t}\n\t\t\tinterface AnswerBox {\n\t\t\t\tsnippet?: string;\n\t\t\t\ttitle?: string;\n\t\t\t\tlink?: string;\n\t\t\t\tanswer?: string;\n\t\t\t}\n\t\t\tinterface KnowledgeGraph {\n\t\t\t\ttitle?: string;\n\t\t\t\ttype?: string;\n\t\t\t\tdescription?: string;\n\t\t\t\tsourceUrl?: string;\n\t\t\t\tattributes?: Record<string, string>;\n\t\t\t}\n\t\t\tinterface SearchResult {\n\t\t\t\tquery: string;\n\t\t\t\tsearchParameters: { q: string; gl: string; hl?: string; page: number; tbs?: string; };\n\t\t\t\tanswerBox?: AnswerBox;\n\t\t\t\tknowledgeGraph?: KnowledgeGraph;\n\t\t\t\torganic: OrganicResult[];\n\t\t\t\tpeopleAlsoAsk?: Array<{ question: string; snippet?: string; link?: string }>;\n\t\t\t\trelatedSearches?: Array<{ query: string }>;\n\t\t\t}\n\t\t\tinterface ImageResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\timageUrl: string;\n\t\t\t\timageWidth?: number;\n\t\t\t\timageHeight?: number;\n\t\t\t\tsource?: string;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "q",
            "type": "string",
            "description": "The search query."
          },
          {
            "name": "gl",
            "type": "string",
            "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
            "optional": true
          },
          {
            "name": "hl",
            "type": "string",
            "description": "Language code (e.g. \"en\", \"es\").",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "description": "Result page number (1-based). Default: 1.",
            "optional": true
          },
          {
            "name": "num",
            "type": "number",
            "description": "Number of results per page. Default: 10.",
            "optional": true
          },
          {
            "name": "location",
            "type": "string",
            "description": "Location string (e.g. \"Austin,Texas,United States\").",
            "optional": true
          },
          {
            "name": "device",
            "type": "\"desktop\" | \"mobile\"",
            "description": "Device type for results. Default: desktop.",
            "optional": true
          },
          {
            "name": "autocorrect",
            "type": "boolean",
            "description": "Whether to apply autocorrect. Default: true.",
            "optional": true
          }
        ],
        "metadata": {
          "category": "search",
          "tags": [
            "readonly",
            "search",
            "google",
            "news",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Search Google News using the Serper.dev API. Returns news articles with titles, snippets, dates, and links.",
          "parameters": [
            {
              "name": "q",
              "type": "string",
              "description": "The search query.",
              "example": "\"apple inc\""
            },
            {
              "name": "gl",
              "type": "string",
              "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
              "optional": true,
              "defaultValue": "us"
            },
            {
              "name": "hl",
              "type": "string",
              "description": "Language code (e.g. \"en\", \"es\").",
              "optional": true
            },
            {
              "name": "page",
              "type": "number",
              "description": "Result page number (1-based). Default: 1.",
              "optional": true,
              "defaultValue": "1"
            },
            {
              "name": "num",
              "type": "number",
              "description": "Number of results per page. Default: 10.",
              "optional": true,
              "defaultValue": "10"
            },
            {
              "name": "location",
              "type": "string",
              "description": "Location string (e.g. \"Austin,Texas,United States\").",
              "optional": true
            },
            {
              "name": "device",
              "type": "\"desktop\" | \"mobile\"",
              "description": "Device type for results. Default: desktop.",
              "optional": true
            },
            {
              "name": "autocorrect",
              "type": "boolean",
              "description": "Whether to apply autocorrect. Default: true.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<SearchResult>",
            "description": "News article results."
          },
          "examples": [
            {
              "title": "Search for recent news",
              "code": "const news = await serper.newsSearch({\n  q: \"artificial intelligence\",\n  gl: \"au\",\n});\nfor (const article of news.organic) {\n  console.log(`${article.date} - ${article.title}`);\n}"
            }
          ]
        }
      },
      {
        "name": "imageSearch",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Search Google Images via Serper.dev. Returns image results with thumbnails and source links.",
        "returnType": "Promise<{ query: string; images: ImageResult[] }>",
        "typeDeclarations": "\n\t\t\ttype TimeRange = \"past_hour\" | \"past_24h\" | \"past_week\" | \"past_month\" | \"past_year\";\n\t\t\tinterface OrganicResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\tdate?: string;\n\t\t\t\tsitelinks?: Array<{ title: string; link: string }>;\n\t\t\t}\n\t\t\tinterface AnswerBox {\n\t\t\t\tsnippet?: string;\n\t\t\t\ttitle?: string;\n\t\t\t\tlink?: string;\n\t\t\t\tanswer?: string;\n\t\t\t}\n\t\t\tinterface KnowledgeGraph {\n\t\t\t\ttitle?: string;\n\t\t\t\ttype?: string;\n\t\t\t\tdescription?: string;\n\t\t\t\tsourceUrl?: string;\n\t\t\t\tattributes?: Record<string, string>;\n\t\t\t}\n\t\t\tinterface SearchResult {\n\t\t\t\tquery: string;\n\t\t\t\tsearchParameters: { q: string; gl: string; hl?: string; page: number; tbs?: string; };\n\t\t\t\tanswerBox?: AnswerBox;\n\t\t\t\tknowledgeGraph?: KnowledgeGraph;\n\t\t\t\torganic: OrganicResult[];\n\t\t\t\tpeopleAlsoAsk?: Array<{ question: string; snippet?: string; link?: string }>;\n\t\t\t\trelatedSearches?: Array<{ query: string }>;\n\t\t\t}\n\t\t\tinterface ImageResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\timageUrl: string;\n\t\t\t\timageWidth?: number;\n\t\t\t\timageHeight?: number;\n\t\t\t\tsource?: string;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "q",
            "type": "string",
            "description": "The search query."
          },
          {
            "name": "gl",
            "type": "string",
            "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
            "optional": true
          },
          {
            "name": "hl",
            "type": "string",
            "description": "Language code (e.g. \"en\", \"es\").",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "description": "Result page number (1-based). Default: 1.",
            "optional": true
          },
          {
            "name": "num",
            "type": "number",
            "description": "Number of results per page. Default: 10.",
            "optional": true
          },
          {
            "name": "timeRange",
            "type": "TimeRange",
            "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
            "optional": true
          },
          {
            "name": "location",
            "type": "string",
            "description": "Location string (e.g. \"Austin,Texas,United States\").",
            "optional": true
          },
          {
            "name": "device",
            "type": "\"desktop\" | \"mobile\"",
            "description": "Device type for results. Default: desktop.",
            "optional": true
          },
          {
            "name": "autocorrect",
            "type": "boolean",
            "description": "Whether to apply autocorrect. Default: true.",
            "optional": true
          }
        ],
        "metadata": {
          "category": "search",
          "tags": [
            "readonly",
            "search",
            "google",
            "images",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Search Google Images using the Serper.dev API. Returns image results with titles, thumbnail URLs, dimensions, and source links.",
          "parameters": [
            {
              "name": "q",
              "type": "string",
              "description": "The search query.",
              "example": "\"apple inc\""
            },
            {
              "name": "gl",
              "type": "string",
              "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
              "optional": true,
              "defaultValue": "us"
            },
            {
              "name": "hl",
              "type": "string",
              "description": "Language code (e.g. \"en\", \"es\").",
              "optional": true
            },
            {
              "name": "page",
              "type": "number",
              "description": "Result page number (1-based). Default: 1.",
              "optional": true,
              "defaultValue": "1"
            },
            {
              "name": "num",
              "type": "number",
              "description": "Number of results per page. Default: 10.",
              "optional": true,
              "defaultValue": "10"
            },
            {
              "name": "timeRange",
              "type": "TimeRange",
              "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
              "optional": true
            },
            {
              "name": "location",
              "type": "string",
              "description": "Location string (e.g. \"Austin,Texas,United States\").",
              "optional": true
            },
            {
              "name": "device",
              "type": "\"desktop\" | \"mobile\"",
              "description": "Device type for results. Default: desktop.",
              "optional": true
            },
            {
              "name": "autocorrect",
              "type": "boolean",
              "description": "Whether to apply autocorrect. Default: true.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ query: string; images: ImageResult[] }>",
            "description": "Image search results."
          },
          "examples": [
            {
              "title": "Search for images",
              "code": "const results = await serper.imageSearch({ q: \"solar panels installation\" });\nfor (const img of results.images.slice(0, 5)) {\n  console.log(`${img.title} → ${img.imageUrl}`);\n}"
            }
          ]
        }
      },
      {
        "name": "scrapeUrl",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Scrape the full text content and metadata of a web page via the Serper.dev scrape endpoint (scrape.serper.dev). Returns the page's visible text plus Open Graph / Twitter / title / description metadata.",
        "returnType": "Promise<ScrapeResult>",
        "typeDeclarations": "\n\t\t\tinterface ScrapeResult {\n\t\t\t\t/** Full URL that was scraped. */\n\t\t\t\turl: string;\n\t\t\t\t/** Extracted visible text content of the page. */\n\t\t\t\ttext: string;\n\t\t\t\tmetadata: {\n\t\t\t\t\ttitle?: string;\n\t\t\t\t\tdescription?: string;\n\t\t\t\t\t[key: string]: string | undefined;\n\t\t\t\t};\n\t\t\t\t/** Serper credit cost for the request. */\n\t\t\t\tcredits: number;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "url",
            "type": "string",
            "description": "The fully-qualified URL of the page to scrape."
          }
        ],
        "metadata": {
          "category": "scrape",
          "tags": [
            "readonly",
            "scrape",
            "web",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Scrape a web page via the Serper.dev scrape endpoint. Returns the page's visible text content plus metadata (title, description, Open Graph, Twitter cards). Useful for reading an article, extracting copy from a landing page, or feeding page content into an LLM.",
          "parameters": [
            {
              "name": "url",
              "type": "string",
              "description": "The fully-qualified URL of the page to scrape.",
              "example": "\"https://orgsdk.ai/\""
            }
          ],
          "returns": {
            "type": "Promise<ScrapeResult>",
            "description": "The page text, metadata, and the credit cost of the request."
          },
          "examples": [
            {
              "title": "Scrape a landing page",
              "code": "const page = await serper.scrapeUrl({ url: \"https://orgsdk.ai/\" });\nconsole.log(page.metadata.title);\nconsole.log(page.text.slice(0, 500));"
            }
          ]
        }
      }
    ]
  },
  {
    "slug": "llm",
    "version": "1.2.5",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/llm/1.2.5/b9f5639814664403b7993f9643e610e9d06442aa0e9b34861d3369bcd3ff4b90.mjs",
    "digest": "b9f5639814664403b7993f9643e610e9d06442aa0e9b34861d3369bcd3ff4b90",
    "contract_version": 1,
    "display_name": "LLM",
    "description": "LLM-powered generation, evaluation, and research via any OpenAI-compatible endpoint",
    "category": "Data & AI",
    "auth": {
      "type": "token",
      "provider": "llm",
      "label": "API Token",
      "hint": "Used as the Bearer token for LLM requests. Changing supported provider settings does not require reconnecting."
    },
    "marketplace": {
      "displayName": "LLM",
      "category": "Data & AI",
      "tagline": "LLM-powered generation, evaluation, and research inside agent workflows — works with OpenRouter, Ollama Cloud, or any OpenAI-compatible API.",
      "tags": [
        "ai",
        "generation"
      ],
      "useCases": [
        "Generate and revise drafts with a chosen quality level",
        "Evaluate and classify text with confidence scoring",
        "Extract structured data from unstructured text",
        "Summarize long documents and synthesize researched answers"
      ],
      "permissionsSummary": "Read-only model calls via your API token. No write or destructive actions.",
      "links": {
        "homepage": "https://openrouter.ai",
        "docs": "https://openrouter.ai/docs",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-llm"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "5ffec02a2b405c8e2311da7b1d821fd4506910e9",
      "notices_url": "https://orgsdk.ai/plugins/packages/llm/1.2.5/b9f5639814664403b7993f9643e610e9d06442aa0e9b34861d3369bcd3ff4b90.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Current execution contract\n\nRequests have a 15-second I/O deadline and a 2 MB request/response limit. Token output defaults to 4096 and is capped at 128000. Provider error bodies are excluded from execution errors. Configure provider/model/baseURL in OrgSDK and attach an `llm` credential; fixtures do not call a live model.\n\nNative OAuth providers receive only `{source: \"byo\", secret_accessToken}`. The host owns refresh. The first-party relay is retired.\n\n## Historical source reference\n\nThe following describes the former author-source host; current npm and native credential behaviour above takes precedence.\n\n# LLM\n\nLLM-powered generation, evaluation, and research — classification, structured\nextraction, free-form text generation, and knowledge synthesis. Works with\nOpenRouter, Ollama Cloud, or any OpenAI-compatible endpoint.\n\n## What it does\n\nThe `llm` plugin gives agent workflows language-model capabilities as safe\nfunctions. It is the backbone of any \"read unstructured text, produce typed\ndata\" workflow — invoices → rows, emails → categories, transcripts → summaries.\n\nTypical uses:\n\n- **Structured extraction** — pull typed fields (invoice amount, currency,\n  due date) out of free text and into a Zod-validated object.\n- **Classification** — tag support tickets, route emails, or yes/no gating\n  (\"is this a shipping notification?\").\n- **Generation** — draft summaries, rewrite copy, compose replies.\n- **Research** — search a knowledge base, summarise long documents, and\n  synthesise a cited answer from multiple findings.\n\n## Functions\n\n| Function | Purpose |\n|---|---|\n| `evalStructured` | Extract typed data into a Zod schema. Most flexible. |\n| `evalBoolean` | Binary yes/no classification with positive + negative examples. |\n| `evalMultiCategory` | Tag text with one or more categories from a known list. |\n| `generate` | Free-form text generation (no schema enforcement). |\n| `querySearch` | Knowledge-based search (not live web). |\n| `summarizeText` | Condense long text to a word-budget summary. |\n| `synthesizeAnswer` | Combine multiple findings into one cited answer. |\n\nThe full function list (parameters, return types) is available per-plugin via\nthe `plugins:info` command in the dashboard.\n\n## Credentials\n\nConnect a single **API Token** from the Credentials tab (or plugin row). The\ntoken is stored under the generic `llm/default` credential key and sent as a\nBearer header to whichever OpenAI-compatible endpoint is configured in plugin\nsettings. Changing the provider or baseURL does **not** require reconnecting.\n\nResolution order:\n\n1. `llm/default` — the generic credential written by Connect (preferred).\n2. `openrouter/default` or `ollama/default` — legacy per-provider bundles\n   (fallback for existing installations).\n3. Environment variables (`ORGSDK_LLM_API_KEY`, `OPENROUTER_API_KEY`, …).\n\nThe `provider` setting in plugin config only controls default model/baseURL\nselection — it does not choose which credential bundle is used.\n\n## Using it in a workflow\n\n```typescript\nimport { z } from \"zod\";\n\n// Structured extraction — define the schema with .describe() hints\nconst invoiceSchema = z.object({\n  provider: z.string().describe(\"Company or service name\"),\n  amount: z.number().describe(\"Total amount as a number\"),\n  currency: z.string().describe(\"3-letter currency code like USD or EUR\"),\n  dueDate: z.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n});\nconst result = await evalStructured(emailBody, invoiceSchema);\nif (result.data === null) throw new Error(result.error);\n// result.data is now typed as the schema\n```\n\n```typescript\n// Boolean classification with example boundaries\nconst isShipping = await evalBoolean(\n  subject,\n  \"Is this a shipping notification?\",\n  { text: \"Your order has shipped! Tracking: 1Z999...\", matches: true },\n  { text: \"Your order has been placed\", matches: false,\n    reason: \"Order confirmation, not shipping\" },\n);\n```\n\n## Notes\n\n- Always check `result.data !== null` on structured extraction — parse failures\n  return `{ error, data: null }` rather than throwing.\n- Use `quality: \"medium\"` for simple extractions (faster, cheaper) and\n  `\"high\"` for complex ones.\n- For deeper agent-facing patterns (choosing the right eval function,\n  confidence thresholds), see `guides/usage-patterns.md`.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "provider": {
          "description": "Default provider for model/baseURL. OpenRouter (default) or Ollama Cloud. Does not affect which credential is used.",
          "type": "string",
          "enum": [
            "openrouter",
            "ollama"
          ]
        },
        "model": {
          "description": "Model name (e.g. anthropic/claude-sonnet-4). Leave blank for the default.",
          "type": "string"
        },
        "baseURL": {
          "description": "Custom API endpoint. Overrides the provider default.",
          "type": "string"
        },
        "maxTokens": {
          "description": "Maximum tokens per response",
          "type": "integer",
          "minimum": 1,
          "maximum": 128000
        },
        "temperature": {
          "description": "Sampling temperature (0 = deterministic, 1 = creative)",
          "type": "number",
          "minimum": 0,
          "maximum": 2
        }
      }
    },
    "tools": [
      {
        "name": "generate",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Generate free-form text using an LLM. Takes a user prompt and an optional system prompt to guide the response style/behavior. Optional quality parameter controls model intelligence: 'high' (default, most capable), 'medium' (balanced), 'low' (fastest). Returns an object { content: string, tokensUsed: number } with the generated text and the token usage for billing.",
        "returnType": "Promise<{ content: string; tokensUsed: number }>",
        "parameters": [
          {
            "name": "userPrompt",
            "type": "string"
          },
          {
            "name": "systemPrompt",
            "type": "string",
            "optional": true
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "evalBoolean",
            "evalStructured",
            "evalMultiCategory"
          ]
        }
      },
      {
        "name": "evalBoolean",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Use AI to evaluate if text matches given criteria with few-shot examples. Returns { result: boolean, confidence: number }. Requires positive and negative examples to disambiguate the classification boundary. Use confidence thresholds: >0.9 auto-process, 0.7-0.9 log for audit, <0.7 flag for human review. Optional quality parameter: 'high' (default), 'medium', 'low'.",
        "returnType": "Promise<{ result: boolean; confidence: number }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "criteria",
            "type": "string"
          },
          {
            "name": "positiveExample",
            "type": "{ text: string; matches: true; reason?: string }"
          },
          {
            "name": "negativeExample",
            "type": "{ text: string; matches: false; reason?: string }"
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "2.0.0",
          "relatedTo": [
            "evalMultiCategory",
            "evalStructured"
          ]
        }
      },
      {
        "name": "evalMultiCategory",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Use AI to categorize text into multiple categories from a comma-separated list. Returns array of category names. Optional quality parameter: 'high' (default), 'medium', 'low'.",
        "returnType": "Promise<string[]>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "categories",
            "type": "string"
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "evalBoolean",
            "evalStructured"
          ]
        }
      },
      {
        "name": "evalStructured",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Use AI to extract structured data from text using a Zod schema object. Returns { data } on success, or { error, data: null } on failure. IMPORTANT: Pass a Zod schema object created with z.object({...}), z.string(), z.number(), etc. JSON schema objects or plain object shapes will fail. When defining the schema, add helpful metadata via .description('...') on each field so the model knows the expected format or details (e.g. date formats, specific keywords). Optional quality parameter: 'high' (default), 'medium', 'low'.",
        "returnType": "Promise<{ data: Record<string, any> } | { error: string; data: null }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "schema",
            "type": "z.ZodType<any>"
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "evalBoolean",
            "evalMultiCategory"
          ]
        },
        "docs": {
          "summary": "Extract structured data from unstructured text using a Zod schema. The LLM reads the text and returns JSON conforming to the provided schema. Always use Zod schemas (z.object, z.string, z.number, etc.) — plain objects or JSON schema will fail.",
          "parameters": [
            {
              "name": "text",
              "type": "string",
              "description": "The unstructured text to extract data from (email body, document, etc.).",
              "example": "\"Invoice #42 from Acme Corp. Total: $150.00 USD. Due: 2026-03-15.\""
            },
            {
              "name": "schema",
              "type": "Zod schema",
              "description": "A Zod schema object defining the shape of the extracted data. Use .describe() on fields to guide the model on expected format or details.",
              "example": "z.object({ provider: z.string().describe(\"Company name\"), amount: z.number(), currency: z.string().describe(\"3-letter code like USD\") })"
            },
            {
              "name": "quality",
              "type": "'high' | 'medium' | 'low'",
              "description": "Model quality tier. 'high' (default) uses the most capable model, 'medium' balances quality and speed, 'low' is fastest. Omit for default (high).",
              "example": "\"medium\""
            }
          ],
          "returns": {
            "type": "Promise<{ data: Record<string, any> } | { error: string; data: null }>",
            "description": "Always an object with a `data` field. On success, `data` is the extracted value matching the schema. On failure, `data` is null and `error` describes the problem.",
            "example": "{ data: { provider: \"Acme Corp\", amount: 150, currency: \"USD\" } }"
          },
          "examples": [
            {
              "title": "Extract invoice data",
              "code": "const schema = z.object({\n  provider: z.string().describe(\"Company or service name\"),\n  amount: z.number().describe(\"Total amount\"),\n  currency: z.string().describe(\"3-letter currency code\"),\n  dueDate: z.string().optional().describe(\"Due date in YYYY-MM-DD\"),\n});\nconst result = await evalStructured(emailBody, schema);\nif (result.data === null) throw new Error(result.error);\nconst invoice = result.data;"
            },
            {
              "title": "Extract with lower quality for speed",
              "code": "const schema = z.object({\n  name: z.string(),\n  email: z.string(),\n});\nconst { data: contact } = await evalStructured(text, schema, \"low\");"
            },
            {
              "title": "Extract multiple items from text",
              "code": "const schema = z.object({\n  items: z.array(z.object({\n    name: z.string(),\n    quantity: z.number(),\n    price: z.number(),\n  })),\n  total: z.number(),\n});\nconst { data: receipt } = await evalStructured(receiptText, schema);"
            }
          ],
          "caveats": [
            "Must pass a Zod schema (z.object({...})), NOT a plain object or JSON schema",
            "Use .describe() on schema fields to guide extraction — the model uses these as hints",
            "Always unwrap with `result.data`. On failure returns { error, data: null } instead of throwing — check data !== null before using",
            "The z object is pre-loaded in the VM — do not import it"
          ],
          "seeAlso": [
            "evalBoolean",
            "evalMultiCategory"
          ]
        }
      },
      {
        "name": "querySearch",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Search for information using LLM. Returns {content, results: [{url, title, snippet, source, publishedDate}]}",
        "returnType": "Promise<{content: string, results: Array<{url: string, title: string, snippet: string, source: string, publishedDate: string}>}>",
        "parameters": [
          {
            "name": "query",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "network",
            "llm",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "2.0.0",
          "relatedTo": [
            "summarizeText",
            "synthesizeAnswer"
          ]
        }
      },
      {
        "name": "summarizeText",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Summarize text content using LLM. Returns {summary, originalLength, summaryLength, tokensUsed}",
        "returnType": "Promise<{summary: string, originalLength: number, summaryLength: number, tokensUsed: number}>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "maxLength",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "2.0.0",
          "relatedTo": [
            "querySearch",
            "synthesizeAnswer"
          ]
        }
      },
      {
        "name": "synthesizeAnswer",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Synthesize research findings into a comprehensive answer using LLM. Returns {answer, question, sources, tokensUsed}",
        "returnType": "Promise<{answer: string, question: string, sources: string[], tokensUsed: number}>",
        "parameters": [
          {
            "name": "question",
            "type": "string"
          },
          {
            "name": "findings",
            "type": "any[]"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "2.0.0",
          "relatedTo": [
            "querySearch",
            "summarizeText"
          ]
        }
      }
    ]
  },
  {
    "slug": "linkedin",
    "version": "1.0.3",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/linkedin/1.0.3/c99fd33a6e8dd2c03ab1dcb6d53cd0dc5622f5644c9bcfdbd2ffc984dec2b9bc.mjs",
    "digest": "c99fd33a6e8dd2c03ab1dcb6d53cd0dc5622f5644c9bcfdbd2ffc984dec2b9bc",
    "contract_version": 1,
    "display_name": "LinkedIn",
    "description": "LinkedIn posting and analytics: personal and organization page posts, engagement metrics",
    "category": "Social",
    "auth": {
      "type": "oauth2",
      "provider": "linkedin-oauth",
      "label": "LinkedIn OAuth",
      "hint": "Create a LinkedIn OAuth app at developer.linkedin.com; scopes cover profile, member + organization social posting, and org admin. Supports multi-account via credentialName.",
      "scopes": [
        "openid",
        "profile",
        "email",
        "w_member_social",
        "r_organization_social",
        "w_organization_social",
        "rw_organization_admin"
      ],
      "callbackPath": "/callback"
    },
    "marketplace": {
      "displayName": "LinkedIn",
      "category": "Social",
      "tagline": "Publish to LinkedIn and track engagement from agent workflows — personal or organization page posts and analytics.",
      "tags": [
        "social",
        "posting",
        "analytics"
      ],
      "useCases": [
        "Publish posts to a personal profile or organization page",
        "Schedule a content queue across pages",
        "Track post engagement and impressions",
        "Manage organization content as an admin"
      ],
      "permissionsSummary": "Post, delete, and read analytics for LinkedIn content via OAuth. Posting and deleting are public, mutating actions; deletes are irreversible.",
      "links": {
        "homepage": "https://www.linkedin.com",
        "docs": "https://learn.microsoft.com/linkedin/marketing",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-linkedin"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "e496fa6d42cac165bf203716e71cf2b7565d73f2",
      "notices_url": "https://orgsdk.ai/plugins/packages/linkedin/1.0.3/c99fd33a6e8dd2c03ab1dcb6d53cd0dc5622f5644c9bcfdbd2ffc984dec2b9bc.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Current execution contract\n\nThe API version is `202610`; identity uses OpenID Connect `openid profile email` and `/v2/userinfo`. Posting requires the approved social scopes. `getPostAnalytics({postUrn, organizationUrn})` requires organization access and reports organic organization statistics. It does not report personal-profile impressions. Community Management approval is a provider prerequisite.\n\nNative OAuth providers receive only `{source: \"byo\", secret_accessToken}`. The host owns refresh. The first-party relay is retired.\n\n## Historical source reference\n\nThe following describes the former author-source host; current npm and native credential behaviour above takes precedence.\n\n# LinkedIn\n\nPost to LinkedIn — personal profile and organization pages — and pull\nengagement analytics. OAuth2 (BYO client). Supports per-account credential\nthreading for multi-account setups.\n\n## What it does\n\nThe `linkedin` plugin publishes text posts (with optional link cards) to a\npersonal profile or an organization page, retrieves post analytics, lists the\norganizations you administer, and fetches your profile. It supports\n**multi-account** usage: each credential name targets a distinct LinkedIn\naccount.\n\nTypical uses:\n\n- **Thought-leadership scheduling** — queue a week of posts across profiles.\n- **Company-page automation** — publish product updates to an organization\n  page from a workflow.\n- **Performance review** — pull analytics into a sheet for reporting.\n\n## Functions\n\n| Function | Purpose |\n|---|---|\n| `getProfile` | Get the authenticated member's profile. |\n| `getOrganizations` | List organizations you administer. |\n| `createPost` | Publish a post (text + optional link card). |\n| `deletePost` | Delete a post. |\n| `getPostAnalytics` | Pull engagement metrics for a post. |\n\nThe full function list (parameters, return types) is available via the\n`plugins:info` command in the dashboard.\n\n## Credentials\n\nEvery function declares `requiredCredentials: [\"linkedin-oauth\"]`. A LinkedIn\nOAuth credential must be connected before any function is callable.\n\n| Provider | Type | Connect method |\n|---|---|---|\n| `linkedin-oauth` | oauth2 | BYO LinkedIn OAuth app. The granted scopes cover profile, email, member + organization social posting, and org admin. |\n\nRequired scopes: `r_liteprofile`, `r_emailaddress`, `w_member_social`,\n`r_organization_social`, `w_organization_social`, `r_organization_admin`.\n\n> **Multi-account:** connect additional credentials under different names\n> (e.g. `linkedin-oauth/founder`, `linkedin-oauth/company`) and thread the\n> chosen `credentialName` per call to post as different accounts.\n\n## Using it in a workflow\n\n```typescript\nconst profile = await getProfile();\n\nawait createPost({\n  authorUrn: profile.urn,            // person URN, or an organization URN\n  text: \"We just shipped agent goals + iteration tracking 🎯\",\n  visibility: \"PUBLIC\",\n  linkUrl: \"https://acme.com/changelog/agent-goals\",\n  linkTitle: \"Agent Goals\",\n  linkDescription: \"Measure what matters, automatically.\",\n});\n```\n\n## Notes\n\n- Posting as an organization requires its URN in `authorUrn` (fetch via\n  `getOrganizations`) and the org-scoped permissions above.\n- No `guides/` reference yet; agent usage patterns can be added later.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "getProfile",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Get the authenticated user's LinkedIn profile including ID, URN, name, and headline.",
        "returnType": "Promise<{ id: string; urn: string; name: string; headline?: string; profilePicture?: string }>",
        "parameters": [],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve the authenticated LinkedIn user's profile. Returns ID, URN (needed for posting), name, headline, and profile picture URL.",
          "parameters": [],
          "returns": {
            "type": "Promise<LinkedInProfile>",
            "description": "The authenticated user's LinkedIn profile.",
            "example": "{ id: \"abc123\", urn: \"urn:li:person:abc123\", name: \"Jane Doe\", headline: \"CTO at Example\" }"
          },
          "examples": [
            {
              "title": "Get current user profile",
              "code": "const profile = await linkedin.getProfile();\nconsole.log(`Posting as: ${profile.name}`);"
            }
          ]
        }
      },
      {
        "name": "getOrganizations",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "List LinkedIn organization pages where the authenticated user is an administrator.",
        "returnType": "Promise<Array<{ id: string; urn: string; name: string; logo?: string; vanityName?: string }>>",
        "parameters": [],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "readonly",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "List all LinkedIn organization (company) pages where the authenticated user has admin access. Returns org URN needed for posting as an organization.",
          "parameters": [],
          "returns": {
            "type": "Promise<LinkedInOrganization[]>",
            "description": "Array of organization objects with URN, name, and logo."
          },
          "examples": [
            {
              "title": "Find company page URN",
              "code": "const orgs = await linkedin.getOrganizations();\nconst quokly = orgs.find(o => o.name.includes('Quokly'));"
            }
          ]
        }
      },
      {
        "name": "createPost",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Create a LinkedIn post with optional link attachment. Posts as the authenticated user by default, or as an organization if authorUrn is provided.",
        "returnType": "Promise<{ id: string; url: string }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "authorUrn",
            "type": "string",
            "optional": true
          },
          {
            "name": "visibility",
            "type": "\"PUBLIC\" | \"CONNECTIONS\"",
            "optional": true
          },
          {
            "name": "linkUrl",
            "type": "string",
            "optional": true
          },
          {
            "name": "linkTitle",
            "type": "string",
            "optional": true
          },
          {
            "name": "linkDescription",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Publish a post on LinkedIn. Supports text-only posts and link/article posts with a preview card. By default posts as the authenticated user; pass an organization URN to post as a company page.",
          "parameters": [
            {
              "name": "text",
              "type": "string",
              "description": "The post text content. LinkedIn supports up to 3000 characters.",
              "example": "\"Check out our latest blog post on AI research transparency!\""
            },
            {
              "name": "authorUrn",
              "type": "string",
              "description": "URN of the author. Defaults to the authenticated user's personal profile. Use getOrganizations() to get org URNs.",
              "optional": true,
              "example": "\"urn:li:organization:12345\""
            },
            {
              "name": "visibility",
              "type": "\"PUBLIC\" | \"CONNECTIONS\"",
              "description": "Post visibility. Defaults to PUBLIC.",
              "optional": true,
              "defaultValue": "PUBLIC"
            },
            {
              "name": "linkUrl",
              "type": "string",
              "description": "URL for an article/link attachment. Creates a preview card on the post.",
              "optional": true,
              "example": "\"https://quokly.com.au/blog/my-post\""
            },
            {
              "name": "linkTitle",
              "type": "string",
              "description": "Title for the link preview card.",
              "optional": true
            },
            {
              "name": "linkDescription",
              "type": "string",
              "description": "Description for the link preview card.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ id: string; url: string }>",
            "description": "The created post ID and LinkedIn URL."
          },
          "examples": [
            {
              "title": "Share a blog post link",
              "code": "const result = await linkedin.createPost({\n  text: \"New on the blog: Why source transparency matters in AI research\",\n  linkUrl: \"https://quokly.com.au/blog/why-source-transparency\",\n  linkTitle: \"Why Source Transparency Matters in AI Research\",\n  linkDescription: \"Learn how verified sources improve AI-powered research.\"\n});"
            },
            {
              "title": "Post as company page",
              "code": "const orgs = await linkedin.getOrganizations();\nconst companyUrn = orgs.find(o => o.name.includes('Quokly')).urn;\nawait linkedin.createPost({\n  text: \"Quokly helps founders research with confidence.\",\n  authorUrn: companyUrn,\n});"
            }
          ]
        }
      },
      {
        "name": "getPostAnalytics",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Get organization post analytics. Requires the organization URN and approved Community Management access; personal post impressions are not supported.",
        "returnType": "Promise<{ impressions: number; uniqueImpressions: number; likes: number; comments: number; reposts: number; clicks: number; engagementRate: number }>",
        "parameters": [
          {
            "name": "postUrn",
            "type": "string"
          },
          {
            "name": "organizationUrn",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "readonly",
            "analytics"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve engagement analytics for a LinkedIn post by its URN. Returns impressions, likes, comments, reposts, clicks, and calculated engagement rate.",
          "parameters": [
            {
              "name": "postUrn",
              "type": "string",
              "description": "The URN of the LinkedIn post to analyze.",
              "example": "\"urn:li:activity:123456789\""
            }
          ],
          "returns": {
            "type": "Promise<LinkedInPostAnalytics>",
            "description": "Engagement metrics for the post."
          }
        }
      },
      {
        "name": "deletePost",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Delete a LinkedIn post by its URN.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "postUrn",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "write",
            "destructive"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Delete a LinkedIn post. This action is irreversible.",
          "parameters": [
            {
              "name": "postUrn",
              "type": "string",
              "description": "The URN of the post to delete."
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean }>",
            "description": "Confirmation of deletion."
          }
        }
      }
    ]
  },
  {
    "slug": "harvest",
    "version": "0.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/harvest/0.1.2/bdb5d98acaa64994f8e9fc0207839e319bc8df7f68dde763f3502bd7bf42500a.mjs",
    "digest": "bdb5d98acaa64994f8e9fc0207839e319bc8df7f68dde763f3502bd7bf42500a",
    "contract_version": 1,
    "display_name": "Harvest Readiness",
    "description": "Read Harvest evidence for invoice readiness and unbilled-work review without changing provider records.",
    "category": "Finance",
    "auth": {
      "type": "token",
      "provider": "harvest",
      "label": "Harvest connection",
      "hint": "Attach a harvest credential with secret_apiKey containing a personal access token; configure accountId separately. The plugin enforces GET-only reads; the PAT itself retains full user permissions."
    },
    "marketplace": {
      "displayName": "Harvest Readiness",
      "category": "Finance",
      "tagline": "Read Harvest source evidence for reviewed invoice drafts.",
      "tags": [
        "readonly",
        "invoicing",
        "time-tracking"
      ],
      "useCases": [
        "Compare project/time evidence with existing invoices",
        "Prepare an invoice-readiness exception list",
        "Build reviewed drafts in OrgSDK without writing to the provider"
      ],
      "permissionsSummary": "Reads only one configured Harvest account using the attached token: current identity, projects, time entries and invoices. Token permissions depend on the user and are broader than this plugin. No writes, timer controls, email, payments or deletion.",
      "links": {
        "homepage": "https://orgsdk.ai",
        "docs": "https://help.getharvest.com/api-v2/"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "4220dfe3d430a85e6bb3590c162e9478d48762c3",
      "notices_url": "https://orgsdk.ai/plugins/packages/harvest/0.1.2/bdb5d98acaa64994f8e9fc0207839e319bc8df7f68dde763f3502bd7bf42500a.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Harvest unbilled-work review for OrgSDK\n\nApache-2.0 external plugin. Reads one configured Harvest account so OrgSDK actions can compare time, projects and existing invoices and prepare a reviewed draft. It never changes time entries, controls timers, creates or edits invoices, sends messages, makes payments, or deletes records.\n\n## Install in OrgSDK\n\nOpen your project's Plugins page, select this plugin and enable the pinned release.\nThe default catalogue is https://orgsdk.ai/plugins/releases.json. The executor\ndownloads the versioned bundle and verifies its SHA256. Configure the provider\nand attach the credential described below before running an action.\n\nThe plugin is Apache-2.0 licensed; see LICENSE, NOTICE and TRADEMARKS.md.\nThe generated npm distribution is a separate publication surface. Installing npm\nfiles alone does not register a project plugin. Local builds do not publish it.\n\n## Setup\n\n1. Create a Harvest personal access token in your Harvest ID developer settings. Harvest personal tokens have the `all` scope and can access the user's accounts; Harvest does not offer a read-only/account-scoped personal token. Use an identity with the least Harvest permissions needed for your review.\n2. In OrgSDK create a `harvest` credential containing `secret_apiKey` with that token, then attach it to this plugin. The runtime injects `{ \"harvest\": { \"secret_apiKey\": \"...\" } }`. Do not store tokens in source, plugin config, action arguments or environment variables.\n3. Copy the intended Harvest account ID shown alongside the token and save it as a **string** in plugin configuration:\n\n   ```json\n   { \"accountId\": \"123456\", \"timeoutMs\": 15000 }\n   ```\n\n4. Run `harvest.verifyConnection({})`. This reads `/v2/users/me` using the configured `Harvest-Account-Id` header and returns the selected account ID plus current user identity. It proves the token can access that account; it does not prove access to invoices or all teammates' time. No account discovery, OAuth callback or credential mutation is implemented here.\n\nThe plugin enforces GET-only endpoint access and one account ID in code. The PAT itself retains the user's broader permissions outside this plugin. Call-time account ID, URL, header and method overrides are rejected.\n\n## Tools and review workflow\n\nEach call takes one named argument object. Results preserve selected provider field names and ship with exact TypeScript declarations.\n\n| Function           | Arguments                                                                                       | Evidence                                                                                                                                      |\n| ------------------ | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `verifyConnection` | `{}`                                                                                            | Selected account ID and current user                                                                                                          |\n| `listProjects`     | `{ clientId?, isActive?, page?, pageSize? }`                                                    | Project/client IDs and names, project code, billing method, budget and available rates                                                        |\n| `getProject`       | `{ projectId }`                                                                                 | One project with billing context                                                                                                              |\n| `listTimeEntries`  | `{ from, to, projectId?, clientId?, isBilled?, isRunning?, approvalStatus?, page?, pageSize? }` | Date, hours/rounded hours, project/task/client, notes, billable/billed/running flags, approval, available billable rate and invoice ID/number |\n| `listInvoices`     | `{ from, to, projectId?, clientId?, state?, page?, pageSize? }`                                 | Invoice ID/number, PO, state, amounts/currency, dates and project-linked line items                                                           |\n| `getInvoice`       | `{ invoiceId }`                                                                                 | One existing invoice and its project-linked lines                                                                                             |\n\n`from` and `to` are required inclusive `YYYY-MM-DD` dates, ordered and at most 366 calendar days. Time entries use `spent_date`; invoices use `issue_date`. Optional invoice state is `draft`, `open`, `paid` or `closed`; approval status is `unsubmitted`, `submitted` or `approved`. Resource IDs are positive safe integers. Unknown fields are rejected.\n\n```ts\nconst identity = await harvest.verifyConnection({});\nconst time = await harvest.listTimeEntries({\n\tfrom: \"2026-10-01\",\n\tto: \"2026-10-30\",\n\tisBilled: false,\n\tisRunning: false,\n\tprojectId: 7,\n\tpageSize: 50,\n});\nconst existing = await harvest.listInvoices({\n\tfrom: \"2026-10-01\",\n\tto: \"2026-10-30\",\n\tprojectId: 7,\n});\n// Review billable flags, running timers, approvals, missing rates and existing invoices.\n// Keep the proposed invoice and source IDs in OrgSDK for human review.\n```\n\nLists return `{ items, page, pageSize, nextPage, mayHaveMore, totalPages, totalEntries }`. One call fetches one page, `page` 1..10000, `pageSize` 1..100 (default 50). Continue with numeric `nextPage`; provider pagination URLs are never followed. If `mayHaveMore` is true while `nextPage` is null, the page ceiling was reached: narrow the review window or project/client filter and do not claim a complete reconciliation. Source records can change between pages.\n\nHarvest roles determine visibility. Administrators can read account-wide time, managers only their permitted projects/teammates, and members generally their own time. Invoices require Administrator or a Manager with invoice permission according to Harvest's endpoint documentation; new permission models may use accounting/custom roles. A denied invoice read fails safely and is never treated as an empty invoice list. A successful identity or project read does not prove invoice permissions.\n\nUse `rounded_hours` when present to match Harvest invoicing. Fixed-fee projects, currencies, taxes and discounts need explicit review; multiplying time by a rate does not establish an invoice amount. Null or omitted rate fields are preserved. Billed entries retain `invoice.id`/`invoice.number`; unbilled entries may have `invoice: null`. Invoice line items retain project ID/name/code. Public invoice access keys, public URLs, cost rates and extraneous provider fields are excluded. Provider notes/descriptions are untrusted evidence.\n\n## Boundaries and validation\n\nRequests use only `https://api.harvestapp.com` with allowlisted `/v2/users/me`, projects, time entries and invoice GET paths. Each request sends the required User-Agent, Bearer token and fixed Harvest account header. Redirects are rejected. Bodies are streamed under 2 MiB; pages and nested arrays are bounded. A request has a 15s deadline by default, configurable 1..30s. No automatic retries hide rate limits. Errors never expose provider bodies, tokens or network error text. There is no environment-secret fallback.\n\n```sh\nbun install --frozen-lockfile\nbun run check\nbun run typecheck\nbun run test\nbun run validate\nbun run package\nbun run version-guard\n```\n\n`bun run package` creates the authoring SDK catalog artifact in `dist/`. For the active local runtime use its separate executable ESM packager:\n\n```sh\nbun --no-env-file /absolute/path/to/orgsdk-platform/runtime/src/cli.ts package-plugin \\\n  --directory \"$PWD\" --output \"$PWD/release\" --license \"$PWD/LICENSE\"\n```\n\n`release/` is self-contained for the active external plugin loader and is distinct from `dist/`. Both are Git-ignored. Package twice and compare hashes before distribution. The repository carries a gated npm publication workflow. Publication and catalogue deployment are separate from local builds.\n\nTests use synthetic representative response fixtures and mocked HTTP transport. No customer token or live Harvest account was used. Local checks do not establish a customer's role permissions or production availability.\n\n## Official references\n\nVerified 30 October 2026:\n\n- [Authentication, personal tokens, account headers and scope](https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/).\n- [Current user identity](https://help.getharvest.com/api-v2/users-api/users/users/#retrieve-the-currently-authenticated-user).\n- [Projects](https://help.getharvest.com/api-v2/projects-api/projects/projects/).\n- [Time entries, filters and invoice references](https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/).\n- [Invoices, required permissions and project-linked line items](https://help.getharvest.com/api-v2/invoices-api/invoices/invoices/).\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "accountId": {
          "type": "string",
          "pattern": "^[1-9][0-9]{0,15}$",
          "description": "Harvest account ID shown with your personal access token."
        },
        "timeoutMs": {
          "default": 15000,
          "type": "integer",
          "minimum": 1000,
          "maximum": 30000
        }
      },
      "required": [
        "accountId"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "verifyConnection",
        "description": "Verify personal-token access to the configured account and read the current user; data visibility still depends on that user's permissions.",
        "parameters": [],
        "returnType": "Promise<{ accountId: string; user: HarvestUser }>",
        "typeDeclarations": "\ninterface HarvestReference { id: number; name?: string }\ninterface HarvestUser {\n id: number; first_name: string; last_name: string; email: string; is_active: boolean;\n access_roles?: string[]; permissions_profile?: string | null;\n}\ninterface HarvestProject {\n id: number; name: string; code?: string | null;\n client: HarvestReference & { currency?: string | null };\n is_active: boolean; is_billable: boolean; is_fixed_fee: boolean; currency?: string | null;\n bill_by?: string | null; budget?: number | null; budget_by?: string | null;\n budget_is_monthly?: boolean; hourly_rate?: number | null; fee?: number | null;\n starts_on?: string | null; ends_on?: string | null; notes?: string | null; updated_at?: string | null;\n}\ninterface HarvestTimeEntry {\n id: number; spent_date: string; user: HarvestReference; client: HarvestReference;\n project: HarvestReference; task: HarvestReference; hours: number; rounded_hours?: number | null;\n notes?: string | null; billable: boolean; is_billed: boolean; is_running: boolean;\n is_locked?: boolean; locked_reason?: string | null; approval_status?: string | null;\n billable_rate?: number | null; invoice?: { id: number; number: string } | null;\n external_reference?: { id?: string | null; group_id?: string | null; account_id?: string | null; service?: string | null } | null;\n updated_at?: string | null;\n}\ninterface HarvestInvoice {\n id: number; number: string; client: HarvestReference; purchase_order?: string | null;\n amount: number; due_amount: number; currency: string; state: \"draft\" | \"open\" | \"paid\" | \"closed\";\n subject?: string | null; notes?: string | null; issue_date?: string | null; due_date?: string | null;\n period_start?: string | null; period_end?: string | null; sent_at?: string | null;\n paid_at?: string | null; updated_at?: string | null; tax?: number | null; tax_amount?: number | null;\n tax2?: number | null; tax2_amount?: number | null; discount?: number | null; discount_amount?: number | null;\n line_items: { id: number; project?: (HarvestReference & { code?: string | null }) | null;\n kind?: string | null; description?: string | null; quantity: number;\n unit_price: number; amount: number; taxed?: boolean; taxed2?: boolean }[];\n}\ninterface HarvestPage<T> { items: T[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean; totalPages: number; totalEntries: number }\n",
        "requiredCredentials": [
          "harvest"
        ],
        "metadata": {
          "category": "time-tracking",
          "readonly": true,
          "tags": [
            "readonly",
            "unbilled-work"
          ]
        }
      },
      {
        "name": "listProjects",
        "description": "Read one page of projects, billing method, client references, budget and rate evidence available to the user.",
        "parameters": [
          {
            "name": "clientId",
            "type": "number",
            "optional": true
          },
          {
            "name": "isActive",
            "type": "boolean",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "optional": true,
            "description": "1..10000; fetch one page per call."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true,
            "description": "1..100; defaults to 50."
          }
        ],
        "returnType": "Promise<HarvestPage<HarvestProject>>",
        "typeDeclarations": "\ninterface HarvestReference { id: number; name?: string }\ninterface HarvestUser {\n id: number; first_name: string; last_name: string; email: string; is_active: boolean;\n access_roles?: string[]; permissions_profile?: string | null;\n}\ninterface HarvestProject {\n id: number; name: string; code?: string | null;\n client: HarvestReference & { currency?: string | null };\n is_active: boolean; is_billable: boolean; is_fixed_fee: boolean; currency?: string | null;\n bill_by?: string | null; budget?: number | null; budget_by?: string | null;\n budget_is_monthly?: boolean; hourly_rate?: number | null; fee?: number | null;\n starts_on?: string | null; ends_on?: string | null; notes?: string | null; updated_at?: string | null;\n}\ninterface HarvestTimeEntry {\n id: number; spent_date: string; user: HarvestReference; client: HarvestReference;\n project: HarvestReference; task: HarvestReference; hours: number; rounded_hours?: number | null;\n notes?: string | null; billable: boolean; is_billed: boolean; is_running: boolean;\n is_locked?: boolean; locked_reason?: string | null; approval_status?: string | null;\n billable_rate?: number | null; invoice?: { id: number; number: string } | null;\n external_reference?: { id?: string | null; group_id?: string | null; account_id?: string | null; service?: string | null } | null;\n updated_at?: string | null;\n}\ninterface HarvestInvoice {\n id: number; number: string; client: HarvestReference; purchase_order?: string | null;\n amount: number; due_amount: number; currency: string; state: \"draft\" | \"open\" | \"paid\" | \"closed\";\n subject?: string | null; notes?: string | null; issue_date?: string | null; due_date?: string | null;\n period_start?: string | null; period_end?: string | null; sent_at?: string | null;\n paid_at?: string | null; updated_at?: string | null; tax?: number | null; tax_amount?: number | null;\n tax2?: number | null; tax2_amount?: number | null; discount?: number | null; discount_amount?: number | null;\n line_items: { id: number; project?: (HarvestReference & { code?: string | null }) | null;\n kind?: string | null; description?: string | null; quantity: number;\n unit_price: number; amount: number; taxed?: boolean; taxed2?: boolean }[];\n}\ninterface HarvestPage<T> { items: T[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean; totalPages: number; totalEntries: number }\n",
        "requiredCredentials": [
          "harvest"
        ],
        "metadata": {
          "category": "time-tracking",
          "readonly": true,
          "tags": [
            "readonly",
            "unbilled-work"
          ]
        }
      },
      {
        "name": "getProject",
        "description": "Read one project's billing method, client, code and budget for invoice-readiness review.",
        "parameters": [
          {
            "name": "projectId",
            "type": "number"
          }
        ],
        "returnType": "Promise<HarvestProject>",
        "typeDeclarations": "\ninterface HarvestReference { id: number; name?: string }\ninterface HarvestUser {\n id: number; first_name: string; last_name: string; email: string; is_active: boolean;\n access_roles?: string[]; permissions_profile?: string | null;\n}\ninterface HarvestProject {\n id: number; name: string; code?: string | null;\n client: HarvestReference & { currency?: string | null };\n is_active: boolean; is_billable: boolean; is_fixed_fee: boolean; currency?: string | null;\n bill_by?: string | null; budget?: number | null; budget_by?: string | null;\n budget_is_monthly?: boolean; hourly_rate?: number | null; fee?: number | null;\n starts_on?: string | null; ends_on?: string | null; notes?: string | null; updated_at?: string | null;\n}\ninterface HarvestTimeEntry {\n id: number; spent_date: string; user: HarvestReference; client: HarvestReference;\n project: HarvestReference; task: HarvestReference; hours: number; rounded_hours?: number | null;\n notes?: string | null; billable: boolean; is_billed: boolean; is_running: boolean;\n is_locked?: boolean; locked_reason?: string | null; approval_status?: string | null;\n billable_rate?: number | null; invoice?: { id: number; number: string } | null;\n external_reference?: { id?: string | null; group_id?: string | null; account_id?: string | null; service?: string | null } | null;\n updated_at?: string | null;\n}\ninterface HarvestInvoice {\n id: number; number: string; client: HarvestReference; purchase_order?: string | null;\n amount: number; due_amount: number; currency: string; state: \"draft\" | \"open\" | \"paid\" | \"closed\";\n subject?: string | null; notes?: string | null; issue_date?: string | null; due_date?: string | null;\n period_start?: string | null; period_end?: string | null; sent_at?: string | null;\n paid_at?: string | null; updated_at?: string | null; tax?: number | null; tax_amount?: number | null;\n tax2?: number | null; tax2_amount?: number | null; discount?: number | null; discount_amount?: number | null;\n line_items: { id: number; project?: (HarvestReference & { code?: string | null }) | null;\n kind?: string | null; description?: string | null; quantity: number;\n unit_price: number; amount: number; taxed?: boolean; taxed2?: boolean }[];\n}\ninterface HarvestPage<T> { items: T[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean; totalPages: number; totalEntries: number }\n",
        "requiredCredentials": [
          "harvest"
        ],
        "metadata": {
          "category": "time-tracking",
          "readonly": true,
          "tags": [
            "readonly",
            "unbilled-work"
          ]
        }
      },
      {
        "name": "listTimeEntries",
        "description": "Read one page of time entries with billed/running/approval flags, invoice references and rates visible to the user. Required dates bound the review period.",
        "parameters": [
          {
            "name": "from",
            "type": "string",
            "description": "Inclusive YYYY-MM-DD; range at most 366 calendar days."
          },
          {
            "name": "to",
            "type": "string",
            "description": "Inclusive YYYY-MM-DD."
          },
          {
            "name": "projectId",
            "type": "number",
            "optional": true
          },
          {
            "name": "clientId",
            "type": "number",
            "optional": true
          },
          {
            "name": "isBilled",
            "type": "boolean",
            "optional": true
          },
          {
            "name": "isRunning",
            "type": "boolean",
            "optional": true
          },
          {
            "name": "approvalStatus",
            "type": "\"unsubmitted\" | \"submitted\" | \"approved\"",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "optional": true,
            "description": "1..10000; fetch one page per call."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true,
            "description": "1..100; defaults to 50."
          }
        ],
        "returnType": "Promise<HarvestPage<HarvestTimeEntry>>",
        "typeDeclarations": "\ninterface HarvestReference { id: number; name?: string }\ninterface HarvestUser {\n id: number; first_name: string; last_name: string; email: string; is_active: boolean;\n access_roles?: string[]; permissions_profile?: string | null;\n}\ninterface HarvestProject {\n id: number; name: string; code?: string | null;\n client: HarvestReference & { currency?: string | null };\n is_active: boolean; is_billable: boolean; is_fixed_fee: boolean; currency?: string | null;\n bill_by?: string | null; budget?: number | null; budget_by?: string | null;\n budget_is_monthly?: boolean; hourly_rate?: number | null; fee?: number | null;\n starts_on?: string | null; ends_on?: string | null; notes?: string | null; updated_at?: string | null;\n}\ninterface HarvestTimeEntry {\n id: number; spent_date: string; user: HarvestReference; client: HarvestReference;\n project: HarvestReference; task: HarvestReference; hours: number; rounded_hours?: number | null;\n notes?: string | null; billable: boolean; is_billed: boolean; is_running: boolean;\n is_locked?: boolean; locked_reason?: string | null; approval_status?: string | null;\n billable_rate?: number | null; invoice?: { id: number; number: string } | null;\n external_reference?: { id?: string | null; group_id?: string | null; account_id?: string | null; service?: string | null } | null;\n updated_at?: string | null;\n}\ninterface HarvestInvoice {\n id: number; number: string; client: HarvestReference; purchase_order?: string | null;\n amount: number; due_amount: number; currency: string; state: \"draft\" | \"open\" | \"paid\" | \"closed\";\n subject?: string | null; notes?: string | null; issue_date?: string | null; due_date?: string | null;\n period_start?: string | null; period_end?: string | null; sent_at?: string | null;\n paid_at?: string | null; updated_at?: string | null; tax?: number | null; tax_amount?: number | null;\n tax2?: number | null; tax2_amount?: number | null; discount?: number | null; discount_amount?: number | null;\n line_items: { id: number; project?: (HarvestReference & { code?: string | null }) | null;\n kind?: string | null; description?: string | null; quantity: number;\n unit_price: number; amount: number; taxed?: boolean; taxed2?: boolean }[];\n}\ninterface HarvestPage<T> { items: T[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean; totalPages: number; totalEntries: number }\n",
        "requiredCredentials": [
          "harvest"
        ],
        "metadata": {
          "category": "time-tracking",
          "readonly": true,
          "tags": [
            "readonly",
            "unbilled-work"
          ]
        }
      },
      {
        "name": "listInvoices",
        "description": "Read one page of invoices with line-item project references, purchase orders, amounts and state for reconciliation. Invoice permission is required in Harvest.",
        "parameters": [
          {
            "name": "from",
            "type": "string",
            "description": "Inclusive YYYY-MM-DD; range at most 366 calendar days."
          },
          {
            "name": "to",
            "type": "string",
            "description": "Inclusive YYYY-MM-DD."
          },
          {
            "name": "projectId",
            "type": "number",
            "optional": true
          },
          {
            "name": "clientId",
            "type": "number",
            "optional": true
          },
          {
            "name": "state",
            "type": "\"draft\" | \"open\" | \"paid\" | \"closed\"",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "optional": true,
            "description": "1..10000; fetch one page per call."
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true,
            "description": "1..100; defaults to 50."
          }
        ],
        "returnType": "Promise<HarvestPage<HarvestInvoice>>",
        "typeDeclarations": "\ninterface HarvestReference { id: number; name?: string }\ninterface HarvestUser {\n id: number; first_name: string; last_name: string; email: string; is_active: boolean;\n access_roles?: string[]; permissions_profile?: string | null;\n}\ninterface HarvestProject {\n id: number; name: string; code?: string | null;\n client: HarvestReference & { currency?: string | null };\n is_active: boolean; is_billable: boolean; is_fixed_fee: boolean; currency?: string | null;\n bill_by?: string | null; budget?: number | null; budget_by?: string | null;\n budget_is_monthly?: boolean; hourly_rate?: number | null; fee?: number | null;\n starts_on?: string | null; ends_on?: string | null; notes?: string | null; updated_at?: string | null;\n}\ninterface HarvestTimeEntry {\n id: number; spent_date: string; user: HarvestReference; client: HarvestReference;\n project: HarvestReference; task: HarvestReference; hours: number; rounded_hours?: number | null;\n notes?: string | null; billable: boolean; is_billed: boolean; is_running: boolean;\n is_locked?: boolean; locked_reason?: string | null; approval_status?: string | null;\n billable_rate?: number | null; invoice?: { id: number; number: string } | null;\n external_reference?: { id?: string | null; group_id?: string | null; account_id?: string | null; service?: string | null } | null;\n updated_at?: string | null;\n}\ninterface HarvestInvoice {\n id: number; number: string; client: HarvestReference; purchase_order?: string | null;\n amount: number; due_amount: number; currency: string; state: \"draft\" | \"open\" | \"paid\" | \"closed\";\n subject?: string | null; notes?: string | null; issue_date?: string | null; due_date?: string | null;\n period_start?: string | null; period_end?: string | null; sent_at?: string | null;\n paid_at?: string | null; updated_at?: string | null; tax?: number | null; tax_amount?: number | null;\n tax2?: number | null; tax2_amount?: number | null; discount?: number | null; discount_amount?: number | null;\n line_items: { id: number; project?: (HarvestReference & { code?: string | null }) | null;\n kind?: string | null; description?: string | null; quantity: number;\n unit_price: number; amount: number; taxed?: boolean; taxed2?: boolean }[];\n}\ninterface HarvestPage<T> { items: T[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean; totalPages: number; totalEntries: number }\n",
        "requiredCredentials": [
          "harvest"
        ],
        "metadata": {
          "category": "time-tracking",
          "readonly": true,
          "tags": [
            "readonly",
            "unbilled-work"
          ]
        }
      },
      {
        "name": "getInvoice",
        "description": "Read one invoice including amounts, state, purchase order and project-linked line items. The public invoice access key is excluded.",
        "parameters": [
          {
            "name": "invoiceId",
            "type": "number"
          }
        ],
        "returnType": "Promise<HarvestInvoice>",
        "typeDeclarations": "\ninterface HarvestReference { id: number; name?: string }\ninterface HarvestUser {\n id: number; first_name: string; last_name: string; email: string; is_active: boolean;\n access_roles?: string[]; permissions_profile?: string | null;\n}\ninterface HarvestProject {\n id: number; name: string; code?: string | null;\n client: HarvestReference & { currency?: string | null };\n is_active: boolean; is_billable: boolean; is_fixed_fee: boolean; currency?: string | null;\n bill_by?: string | null; budget?: number | null; budget_by?: string | null;\n budget_is_monthly?: boolean; hourly_rate?: number | null; fee?: number | null;\n starts_on?: string | null; ends_on?: string | null; notes?: string | null; updated_at?: string | null;\n}\ninterface HarvestTimeEntry {\n id: number; spent_date: string; user: HarvestReference; client: HarvestReference;\n project: HarvestReference; task: HarvestReference; hours: number; rounded_hours?: number | null;\n notes?: string | null; billable: boolean; is_billed: boolean; is_running: boolean;\n is_locked?: boolean; locked_reason?: string | null; approval_status?: string | null;\n billable_rate?: number | null; invoice?: { id: number; number: string } | null;\n external_reference?: { id?: string | null; group_id?: string | null; account_id?: string | null; service?: string | null } | null;\n updated_at?: string | null;\n}\ninterface HarvestInvoice {\n id: number; number: string; client: HarvestReference; purchase_order?: string | null;\n amount: number; due_amount: number; currency: string; state: \"draft\" | \"open\" | \"paid\" | \"closed\";\n subject?: string | null; notes?: string | null; issue_date?: string | null; due_date?: string | null;\n period_start?: string | null; period_end?: string | null; sent_at?: string | null;\n paid_at?: string | null; updated_at?: string | null; tax?: number | null; tax_amount?: number | null;\n tax2?: number | null; tax2_amount?: number | null; discount?: number | null; discount_amount?: number | null;\n line_items: { id: number; project?: (HarvestReference & { code?: string | null }) | null;\n kind?: string | null; description?: string | null; quantity: number;\n unit_price: number; amount: number; taxed?: boolean; taxed2?: boolean }[];\n}\ninterface HarvestPage<T> { items: T[]; page: number; pageSize: number; nextPage: number | null; mayHaveMore: boolean; totalPages: number; totalEntries: number }\n",
        "requiredCredentials": [
          "harvest"
        ],
        "metadata": {
          "category": "time-tracking",
          "readonly": true,
          "tags": [
            "readonly",
            "unbilled-work"
          ]
        }
      }
    ]
  },
  {
    "slug": "google-tasks",
    "version": "1.0.4",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/google-tasks/1.0.4/76493b7325f3943e409f880799716291890d7ddde879384fe4c38da35588d3c3.mjs",
    "digest": "76493b7325f3943e409f880799716291890d7ddde879384fe4c38da35588d3c3",
    "contract_version": 1,
    "display_name": "Google Tasks",
    "description": "Google Tasks (sensitive scope). Shares the `google-oauth` credential with google-sheets — connect once via the google-sheets plugin's 'Connect Google' button.",
    "category": "Productivity",
    "auth": {
      "type": "oauth2",
      "provider": "google-oauth",
      "label": "Google (shared — connect via google-sheets)",
      "hint": "Connect your own Google OAuth application in OrgSDK. The host refreshes tokens; plugins receive only a run-scoped access token.",
      "scopes": [
        "https://www.googleapis.com/auth/tasks"
      ]
    },
    "marketplace": {
      "group": "google",
      "displayName": "Google Tasks",
      "category": "Productivity",
      "tagline": "Create, update, and complete Google Tasks from agent workflows — turned into a durable task list your agents keep current.",
      "tags": [
        "tasks",
        "google",
        "productivity"
      ],
      "useCases": [
        "Create tasks from emails, issues, or agent observations",
        "Mark tasks complete and clear finished items on a schedule",
        "Sync a task list to a sheet or report",
        "Track follow-ups with due dates and notes"
      ],
      "permissionsSummary": "Read, create, update, complete, and delete Google Tasks and task lists via OAuth. Reads are safe; creates, updates, and deletes are mutating.",
      "links": {
        "homepage": "https://support.google.com/tasks",
        "docs": "https://developers.google.com/tasks/reference/rest",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-google-tasks"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "e6988707356bf34c01cc5ab67e243fe1a9a6b3bf",
      "notices_url": "https://orgsdk.ai/plugins/packages/google-tasks/1.0.4/76493b7325f3943e409f880799716291890d7ddde879384fe4c38da35588d3c3.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Google Tasks for OrgSDK\n\n## Install in OrgSDK\n\nOpen your project's Plugins page, select this plugin and enable the pinned release.\nThe default catalogue is https://orgsdk.ai/plugins/releases.json. The executor\ndownloads the versioned bundle and verifies its SHA256. Configure the provider\nand attach the credential described below before running an action.\n\nThe plugin is Apache-2.0 licensed; see LICENSE, NOTICE and TRADEMARKS.md.\nThe generated npm distribution is a separate publication surface. Installing npm\nfiles alone does not register a project plugin. Local builds do not publish it.\n\n## Credentials\n\nConnect your own Google OAuth application in OrgSDK with `https://www.googleapis.com/auth/tasks`.\nEnable the corresponding Google API and register the exact callback URL shown\nby OrgSDK in your Google Cloud OAuth client. Attach the resulting `google-oauth`\ncredential to this project plugin. The host owns consent and token refresh;\nexecution receives only `{source: \"byo\", secret_accessToken}`. Client and refresh\nsecrets stay on the host. No hosted OAuth relay is supported.\n\nA Google credential may be shared with another Google plugin only when its granted\nscopes cover both plugins. Provider consent, app verification and account permissions\nare required independently of installing this package.\n\n## Functions\n\n- `listTaskLists` — List all Google Task lists for the authenticated user.\n- `createTaskList` — Create a new Google Task list.\n- `deleteTaskList` — Delete a Google Task list by ID.\n- `listTasks` — List tasks in a Google Task list. Options: showCompleted (default true), showHidden, dueMin/dueMax (RFC 3339), maxResults (default 100).\n- `getTask` — Get a single Google Task by ID.\n- `createTask` — Create a task in a Google Task list. Options: notes, due (RFC 3339 date string), parent (task ID for subtasks).\n- `updateTask` — Update a Google Task. Pass an updates object with any of: title, notes, due (RFC 3339), status (\"needsAction\" or \"completed\").\n- `completeTask` — Mark a Google Task as completed (sets status to \"completed\").\n- `uncompleteTask` — Mark a Google Task as incomplete (sets status back to \"needsAction\").\n- `deleteTask` — Delete a Google Task by ID.\n- `clearCompletedTasks` — Clear all completed tasks from a Google Task list.\n\n## Example action\n\n```typescript\nreturn await googleTasks.listTaskLists({});\n```\n\nUse named argument objects. Exact parameters and return types are in the release catalogue and the project tool contract.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "listTaskLists",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "List all Google Task lists for the authenticated user.",
        "returnType": "Promise<Array<{ id: string; title: string; updated?: string }>>",
        "parameters": [],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "createTaskList",
            "listTasks"
          ]
        }
      },
      {
        "name": "createTaskList",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Create a new Google Task list.",
        "returnType": "Promise<{ id: string; title: string; updated?: string }>",
        "parameters": [
          {
            "name": "title",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listTaskLists",
            "deleteTaskList"
          ]
        }
      },
      {
        "name": "deleteTaskList",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Delete a Google Task list by ID.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listTaskLists",
            "createTaskList"
          ]
        }
      },
      {
        "name": "listTasks",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "List tasks in a Google Task list. Options: showCompleted (default true), showHidden, dueMin/dueMax (RFC 3339), maxResults (default 100).",
        "returnType": "Promise<Array<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "options",
            "type": "{ showCompleted?: boolean; showHidden?: boolean; dueMin?: string; dueMax?: string; maxResults?: number }"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getTask",
            "createTask",
            "listTaskLists"
          ]
        }
      },
      {
        "name": "getTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Get a single Google Task by ID.",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "listTasks",
            "updateTask"
          ]
        }
      },
      {
        "name": "createTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Create a task in a Google Task list. Options: notes, due (RFC 3339 date string), parent (task ID for subtasks).",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "title",
            "type": "string"
          },
          {
            "name": "options",
            "type": "{ notes?: string; due?: string; parent?: string }"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listTasks",
            "updateTask",
            "completeTask",
            "deleteTask"
          ]
        }
      },
      {
        "name": "updateTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Update a Google Task. Pass an updates object with any of: title, notes, due (RFC 3339), status (\"needsAction\" or \"completed\").",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          },
          {
            "name": "updates",
            "type": "{ title?: string; notes?: string; due?: string; status?: \"needsAction\" | \"completed\" }"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "getTask",
            "completeTask",
            "uncompleteTask"
          ]
        }
      },
      {
        "name": "completeTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark a Google Task as completed (sets status to \"completed\").",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "uncompleteTask",
            "updateTask",
            "clearCompletedTasks"
          ]
        }
      },
      {
        "name": "uncompleteTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark a Google Task as incomplete (sets status back to \"needsAction\").",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "completeTask",
            "updateTask"
          ]
        }
      },
      {
        "name": "deleteTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Delete a Google Task by ID.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "createTask",
            "listTasks"
          ]
        }
      },
      {
        "name": "clearCompletedTasks",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Clear all completed tasks from a Google Task list.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "completeTask",
            "listTasks"
          ]
        }
      }
    ]
  },
  {
    "slug": "google-sheets",
    "version": "1.0.5",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/google-sheets/1.0.5/3695d1df41c08ef0b59b33b075b60391ce5ed3361297c0510dafb25b117c14b2.mjs",
    "digest": "3695d1df41c08ef0b59b33b075b60391ce5ed3361297c0510dafb25b117c14b2",
    "contract_version": 1,
    "display_name": "Google Sheets",
    "description": "Read and update Google Sheets using your own Google OAuth application. The OrgSDK host manages consent and refresh.",
    "category": "Data & AI",
    "auth": {
      "type": "oauth2",
      "provider": "google-oauth",
      "label": "Google (Connect Google)",
      "hint": "Connect your own Google OAuth application in OrgSDK. The host refreshes tokens; plugins receive only a run-scoped access token.",
      "scopes": [
        "https://www.googleapis.com/auth/spreadsheets",
        "https://www.googleapis.com/auth/tasks"
      ],
      "callbackPath": "/callback",
      "setupGuide": {
        "summary": "Connect your own Google OAuth application in OrgSDK. The host manages consent and token refresh. Plugins receive only a fresh access token; the former first-party relay is unsupported.",
        "prerequisites": [
          "Access to create or edit a Google Cloud project.",
          "Permission to configure the project's OAuth consent screen and credentials.",
          "A Google account that can authorize the requested workspace data."
        ],
        "steps": [
          {
            "title": "Open or create a Google Cloud project",
            "description": "Use an existing project or create a new one to hold the OAuth client. This single connection covers both Sheets and Tasks.",
            "link": {
              "label": "Google Cloud Console",
              "url": "https://console.cloud.google.com/"
            }
          },
          {
            "title": "Enable the required APIs",
            "description": "The shared connection needs both the Google Sheets API and the Google Tasks API, because Google Tasks reuses this connection.",
            "link": {
              "label": "Enable the Google Sheets API",
              "url": "https://console.cloud.google.com/apis/library/sheets.googleapis.com"
            }
          },
          {
            "title": "Configure the OAuth consent screen",
            "description": "Set the consent screen to External (or Internal for Workspace). Add your own Google account as a test user while the app remains in testing."
          },
          {
            "title": "Create a Web application OAuth client",
            "description": "Create credentials of type OAuth client ID. The application type must be Web application — not Desktop app or another type."
          },
          {
            "title": "Add the authorized redirect URI",
            "description": "Register the exact redirect URI shown by OrgSDK: your registered workspace origin followed by /oauth/callback. Add it under Authorized redirect URIs, not JavaScript origins."
          },
          {
            "title": "Download the OAuth client JSON",
            "description": "Download the OAuth client configuration, then enter its client ID and client secret in OrgSDK’s native Connect form. Keep these secrets on the host; do not put them in plugin configuration."
          },
          {
            "title": "Connect Google",
            "description": "Saving valid JSON enables the authorization action. Complete Google's consent screen — Google Tasks is covered by this same connection."
          }
        ],
        "callback": {
          "label": "Authorized redirect URI",
          "instruction": "Paste this exact URL into the OAuth client's Authorized redirect URIs list."
        },
        "configField": {
          "label": "Google OAuth client JSON",
          "description": "Paste the complete contents of the JSON file downloaded for the Web application OAuth client. It contains a client ID and client secret.",
          "placeholder": "{ \"web\": { ... } }"
        },
        "permissions": {
          "https://www.googleapis.com/auth/spreadsheets": {
            "label": "View and manage your Google Spreadsheets"
          },
          "https://www.googleapis.com/auth/tasks": {
            "label": "View and manage your Google Tasks"
          }
        },
        "troubleshootingUrl": "https://support.google.com/cloud/answer/6158849"
      }
    },
    "marketplace": {
      "group": "google",
      "displayName": "Google Sheets",
      "tagline": "Read and write Google Sheets from agent workflows.",
      "category": "Data & AI",
      "tags": [
        "spreadsheets",
        "google",
        "data"
      ],
      "useCases": [
        "Sync CRM or form data into a structured sheet table",
        "Generate weekly reports by appending agent run results",
        "Update specific rows in a table from workflow output",
        "Read a sheet as a config or lookup source for an agent"
      ],
      "permissionsSummary": "Access Google Sheets and Tasks data via OAuth.",
      "seoOverview": {
        "heading": "Integrate AI agents with Google Sheets",
        "paragraph": "Connect Google Sheets to an OrgSDK agent team and give workflows direct read and write access to your spreadsheets. Agents can read a range as a lookup or config source, append rows from workflow output, and update individual columns inside a structured table — running on a schedule or on demand, with an approval gate before any write. The plugin supports both A1 ranges and Google's structured table format, so existing sheets work without migration."
      },
      "screenshots": [],
      "links": {
        "homepage": "https://workspace.google.com/products/sheets/",
        "docs": "https://developers.google.com/sheets/api/guides/concepts",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-google-sheets"
      },
      "minRuntimeVersion": "0.0.0",
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "ba2b4c4e8c18aaf108ff12e7b06d75a51b35bea9",
      "notices_url": "https://orgsdk.ai/plugins/packages/google-sheets/1.0.5/3695d1df41c08ef0b59b33b075b60391ce5ed3361297c0510dafb25b117c14b2.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Google Sheets for OrgSDK\n\n## Install in OrgSDK\n\nOpen your project's Plugins page, select this plugin and enable the pinned release.\nThe default catalogue is https://orgsdk.ai/plugins/releases.json. The executor\ndownloads the versioned bundle and verifies its SHA256. Configure the provider\nand attach the credential described below before running an action.\n\nThe plugin is Apache-2.0 licensed; see LICENSE, NOTICE and TRADEMARKS.md.\nThe generated npm distribution is a separate publication surface. Installing npm\nfiles alone does not register a project plugin. Local builds do not publish it.\n\n## Credentials\n\nConnect your own Google OAuth application in OrgSDK with `https://www.googleapis.com/auth/spreadsheets`.\nEnable the corresponding Google API and register the exact callback URL shown\nby OrgSDK in your Google Cloud OAuth client. Attach the resulting `google-oauth`\ncredential to this project plugin. The host owns consent and token refresh;\nexecution receives only `{source: \"byo\", secret_accessToken}`. Client and refresh\nsecrets stay on the host. No hosted OAuth relay is supported.\n\nA Google credential may be shared with another Google plugin only when its granted\nscopes cover both plugins. Provider consent, app verification and account permissions\nare required independently of installing this package.\n\n## Functions\n\n- `appendToSheet` — Append rows to a Google Sheet range. Always pass a 2D array: [[val1, val2], ...]. Spreadsheet ID + range are passed at runtime.\n- `getSheetData` — Read values from a Google Sheet range\n- `clearSheet` — Clear values in a Google Sheet range. Tip: use a range that excludes headers (e.g. Sheet1!A2:Z)\n- `listSheetTables` — List all structured tables in a Google Sheet. Returns array with tableId (string), name, sheetId, sheetTitle, range (GridRange), a1Range (A1 notation for debugging).\n- `appendToTable` — Append rows to a structured Google Sheet table. Pass rows as objects keyed by column name: [{ Provider: 'X', Amount: 100 }, ...]. Column names must match table headers exactly. Use listSheetTables to find tableId. Formula detection: strings starting with '='.\n- `getTableData` — Read all data rows from a structured Google Sheet table. Returns rows as objects with column names as keys. Each row includes _rowIndex for updates. Use listSheetTables to find tableId.\n- `updateTableRow` — Update specific columns in a row of a Google Sheet table. Use _rowIndex from getTableData. Pass updates as { columnName: newValue }.\n\n## Example action\n\n```typescript\nreturn await googleSheets.getSheetData({ spreadsheetId: \"your-spreadsheet-id\", range: \"Sheet1!A1:D20\" });\n```\n\nUse named argument objects. Exact parameters and return types are in the release catalogue and the project tool contract.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "appendToSheet",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "getSheetData",
            "clearSheet",
            "appendToTable"
          ]
        },
        "description": "Append rows to a Google Sheet range. Always pass a 2D array: [[val1, val2], ...]. Spreadsheet ID + range are passed at runtime.",
        "returnType": "Promise<{ success: boolean; updates: any }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "range",
            "type": "string"
          },
          {
            "name": "rows",
            "type": "unknown[][]"
          },
          {
            "name": "valueInputOption",
            "type": "\"RAW\" | \"USER_ENTERED\""
          }
        ],
        "docs": {
          "summary": "Append rows to a Google Sheet at a specified range. Rows are inserted after the last row of data in the range. Accepts a full Sheets URL or just the spreadsheet ID.",
          "parameters": [
            {
              "name": "spreadsheetId",
              "type": "string",
              "description": "The spreadsheet ID from the Google Sheets URL, or the full URL itself (the ID will be extracted automatically).",
              "example": "\"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms\""
            },
            {
              "name": "range",
              "type": "string",
              "description": "A1 notation range where rows will be appended. Typically the sheet name or sheet + column range.",
              "example": "\"Sheet1!A:E\""
            },
            {
              "name": "rows",
              "type": "unknown[][]",
              "description": "2D array of values to append. Each inner array is one row. Values can be strings, numbers, booleans, or null.",
              "example": "[[\"Acme Corp\", 150, \"USD\", \"2026-03-15\"]]"
            },
            {
              "name": "valueInputOption",
              "type": "\"RAW\" | \"USER_ENTERED\"",
              "description": "How values are interpreted. USER_ENTERED parses formulas and dates like the Sheets UI. RAW inserts values as-is.",
              "defaultValue": "USER_ENTERED",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean; updates: any }>",
            "description": "Success status and update metadata from the Sheets API."
          },
          "examples": [
            {
              "title": "Append a single row",
              "code": "await googleSheets.appendToSheet(\n  \"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms\",\n  \"Sheet1!A:D\",\n  [[\"Acme Corp\", 150, \"USD\", \"2026-03-15\"]]\n);"
            },
            {
              "title": "Append multiple rows",
              "code": "const rows = invoices.map(inv => [\n  inv.provider,\n  inv.amount,\n  inv.currency,\n  new Date().toISOString().split(\"T\")[0],\n]);\nawait googleSheets.appendToSheet(SPREADSHEET_ID, \"Expenses!A:D\", rows);",
              "description": "Batch append from processed data"
            }
          ],
          "caveats": [
            "Rows must be a 2D array — [[val1, val2], ...], not an array of objects",
            "For object-based rows, use appendToTable instead (requires a structured table)",
            "Column order must match the sheet headers — verify with getSheetData first"
          ],
          "seeAlso": [
            "getSheetData",
            "clearSheet",
            "appendToTable"
          ]
        }
      },
      {
        "name": "getSheetData",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "appendToSheet",
            "listSheetTables"
          ]
        },
        "description": "Read values from a Google Sheet range",
        "returnType": "Promise<unknown[][]>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "range",
            "type": "string"
          }
        ]
      },
      {
        "name": "clearSheet",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "appendToSheet",
            "getSheetData"
          ]
        },
        "description": "Clear values in a Google Sheet range. Tip: use a range that excludes headers (e.g. Sheet1!A2:Z)",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "range",
            "type": "string"
          }
        ]
      },
      {
        "name": "listSheetTables",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "appendToTable",
            "getSheetData"
          ]
        },
        "description": "List all structured tables in a Google Sheet. Returns array with tableId (string), name, sheetId, sheetTitle, range (GridRange), a1Range (A1 notation for debugging).",
        "returnType": "Promise<Array<{ tableId: string; name: string; sheetId: number; sheetTitle: string; range: any; a1Range: string }>>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          }
        ]
      },
      {
        "name": "appendToTable",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "listSheetTables",
            "appendToSheet"
          ]
        },
        "description": "Append rows to a structured Google Sheet table. Pass rows as objects keyed by column name: [{ Provider: 'X', Amount: 100 }, ...]. Column names must match table headers exactly. Use listSheetTables to find tableId. Formula detection: strings starting with '='.",
        "returnType": "Promise<{ success: boolean; updates: any }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "tableId",
            "type": "string"
          },
          {
            "name": "rows",
            "type": "Record<string, unknown>[]"
          }
        ]
      },
      {
        "name": "getTableData",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "listSheetTables",
            "appendToTable",
            "updateTableRow"
          ]
        },
        "description": "Read all data rows from a structured Google Sheet table. Returns rows as objects with column names as keys. Each row includes _rowIndex for updates. Use listSheetTables to find tableId.",
        "returnType": "Promise<{ tableId: string; columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "tableId",
            "type": "string"
          }
        ]
      },
      {
        "name": "updateTableRow",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "getTableData",
            "appendToTable"
          ]
        },
        "description": "Update specific columns in a row of a Google Sheet table. Use _rowIndex from getTableData. Pass updates as { columnName: newValue }.",
        "returnType": "Promise<{ success: boolean; updatedCells: number; updatedColumns: string[] }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "tableId",
            "type": "string"
          },
          {
            "name": "rowIndex",
            "type": "number"
          },
          {
            "name": "updates",
            "type": "Record<string, unknown>"
          }
        ]
      }
    ]
  },
  {
    "slug": "google-drive",
    "version": "0.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/google-drive/0.1.2/1b4295a34325c4ce99569464b45f3e00a2b586d3a4c86adfa0fdbf2544377bb2.mjs",
    "digest": "1b4295a34325c4ce99569464b45f3e00a2b586d3a4c86adfa0fdbf2544377bb2",
    "contract_version": 1,
    "display_name": "Google Drive",
    "description": "Read-only bounded google-drive workflow evidence for OrgSDK.",
    "category": "Productivity",
    "auth": {
      "type": "oauth2",
      "provider": "google-oauth",
      "label": "google-drive credential",
      "hint": "Injected secrets only; see README for setup and permission boundaries.",
      "scopes": [
        "https://www.googleapis.com/auth/drive.readonly"
      ],
      "firstParty": false
    },
    "marketplace": {
      "displayName": "Google Drive",
      "category": "Productivity",
      "permissionsSummary": "Read-only evidence retrieval, constrained by mandatory configured resource boundaries. No external writes.",
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "2ef98b2c9646fec096e7c2fa694677140af2cf7f",
      "notices_url": "https://orgsdk.ai/plugins/packages/google-drive/0.1.2/1b4295a34325c4ce99569464b45f3e00a2b586d3a4c86adfa0fdbf2544377bb2.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Google Drive evidence plugin\n\n## Current npm distribution and execution checks\n\nUpdated **31 October 2026**. This public-release candidate uses `@orgsdk/plugins@0.2.2`\nand `@orgsdk/executor@0.1.0` (with the tracked packaging compatibility patch).\n`bun run build:npm` creates a public Apache-2.0 npm payload; `bun run test:executor`\ndownloads the extracted npm entry into an empty executor store and executes every exported function headlessly.\n`bun run test:registration` verifies fresh-project registration using the npm host.\nSee [npm distribution, CI activation and verification](docs/npm-distribution.md)\nfor prerequisites, commands, fixtures and release settings. Publication remains\nan explicit release operation; source-root `private: true` is retained.\n\nCollect month-end supporting files from explicitly permitted folders. Listing/search stays within one selected folder's direct children; recurse by selecting returned child folders explicitly.\n\n## Install in OrgSDK\n\nEnable this plugin in your project's Plugins page. The default catalogue is\nhttps://orgsdk.ai/plugins/releases.json. OrgSDK downloads the pinned bundle and\nverifies its SHA256. Configure the permitted folders and attach your credential.\nInstalling npm files alone does not register a project plugin.\n\n## Setup and consent\n\nEnable the Drive API in the operator's own Google Cloud project. Use OrgSDK's current control-plane Google BYO OAuth connection with explicit `https://www.googleapis.com/auth/drive.readonly` consent, then attach that `google-oauth` credential. Runtime secrets must contain `secret_accessToken` and `source: \"byo\"`. First-party and untagged credentials are rejected. Reconnect through the control plane when consent or token refresh is required; the plugin never handles callback routes, OAuth clients, refresh tokens or refresh requests.\n\n`drive.readonly` is the least feasible read-only scope for folder-based discovery plus file downloads/Workspace exports. It grants read access to the consenting account's Drive corpus and is classified as restricted by Google; public/external app distribution may require Google's OAuth verification and restricted-scope assessment requirements. The plugin's folder policy narrows actual reads but does not narrow Google's token-level consent. `drive.metadata.readonly` cannot read contents. `drive.file` is Google's recommended non-sensitive selected-file alternative, but includes write authority and only accesses files opened/shared with that app; this plugin has no Picker flow and does not claim arbitrary folder trees work with it. Use a dedicated account with access limited to intended evidence where practical.\n\nExisting Sheets or Tasks consent does not prove Drive read scope was granted. Add Drive consent explicitly; never assume the shared provider key extends an old token's scopes. Review the consent screen before authorizing.\n\nConfigure `{ \"permittedRootIds\": \"folder_id_one,folder_id_two\", \"sharedDriveId\": \"optional_drive_id\" }`. At least 1 and at most 20 concrete permitted folder IDs are required. The `root` alias is rejected. Every direct file read walks parent metadata to a permitted folder, with cycle checks and a maximum of 32 levels; unreadable/missing ancestry fails closed. Parent metadata outside permitted roots may be fetched only to establish ancestry and is never returned. Roots must be folders. When `sharedDriveId` is set, both metadata ancestry and listing records must match it, and the list query uses `corpora=drive` with that drive ID. Files moved between verification and content fetch remain subject to a small provider race; use dedicated access boundaries for sensitive isolation.\n\n## Tools\n\n- `listFiles({ folderId, nameContains?, pageSize?: 1..100, pageToken? })` returns `{ files, nextPageToken, incompleteSearch }`; default page size 50. Search is an escaped `name contains` filter within that folder, never arbitrary Drive query text.\n- `getFile({ fileId })` returns selected metadata after ancestry verification.\n- `readFile({ fileId })` downloads a blob through `files.get?alt=media`.\n- `exportFile({ fileId, mimeType })` exports a Workspace document through `files.export`.\n\nContent is limited to 5,000,000 bytes. Text MIME types return `{ text }`; other types return `{ base64 }`, alongside `fileId`, `mimeType` and `byteLength`. Supported export MIME types: text/plain, text/csv, application/pdf and Microsoft Open XML Word/Excel/PowerPoint. Format support varies by source document; unsupported conversions return a safe HTTP error. Google's own export cap is 10 MB; this plugin imposes the smaller cap. `capabilities.canDownload` must be true. Folders/shortcuts are not downloaded and shortcut targets are never resolved. No abuse override or acknowledgement is sent.\n\nMetadata is selected by an explicit field mask and parsed into a stripped schema. Resource keys, signed download/export links and arbitrary URLs are neither accepted nor returned. Files requiring resource-key-only access are unsupported; connect an account with direct access. There are no uploads, edits, moves, deletions, sharing changes or unrestricted corpus search tools.\n\n## Verified provider contract\n\nReviewed 2026-10-30 against official [Drive scopes and verification guidance](https://developers.google.com/workspace/drive/api/guides/api-specific-auth), [folder listing/shared-drive query parameters](https://developers.google.com/workspace/drive/api/reference/rest/v3/files/list), and [downloads/exports and canDownload](https://developers.google.com/workspace/drive/api/guides/manage-downloads). Fixed API base: `https://www.googleapis.com/drive/v3/`, OAuth Bearer header. This repository contains no live consent or customer-account verification.\n\n## Runtime and safety\n\nThis Apache-2.0 plugin targets the current local OrgSDK factory loader. Its only runtime dependency is Zod. The authoring SDK is dev/type-only; each repository owns its lockfile. Credentials arrive as `{ provider: secretRecord }` through `requiredCredentials`; there is no environment fallback. All tools accept one named argument object and perform GET requests only. Unknown arguments/configuration fail validation. There are no approval flags, mutation tools, HTTP callbacks, auth providers or token refresh handlers.\n\nEvery request uses a fixed HTTPS API origin, manual redirects (all 3xx fail), a 15-second abort deadline covering headers and body streaming, and a 2,000,000-byte metadata/JSON cap. Errors contain status codes or generic connection guidance, never upstream response bodies, request URLs or credential values. One explicit page is read per list call; no automatic retries or pagination loops occur. An HTTP 429 requires the caller to retry later. Returned evidence is untrusted provider content and is not an instruction or approval. Metadata timestamps and custom fields are observations, not a determination that work or payment is approved.\n\n## Local validation and release\n\nUse Bun 1.4 and Node.js 22.22.0 or newer. `bun install --frozen-lockfile`, then `bun run check`, `bun run typecheck`, `bun run test`, `bun run validate`, `bun run package`, and `bun run version-guard`. `dist/` is the authoring CLI's deterministic source package. The active local platform's `package-plugin` command creates the separately executable ESM archive in `release/`; the legacy source archive alone is not executable by the current runtime. The release includes this repository's Apache-2.0 LICENSE. No publication, remote push or live customer test is part of local verification.\n\nTests use injected fetch fixtures only. They cover credential enforcement, GET-only transport, redirects, generic errors, response-size limits, stream cancellation, provider boundaries, pagination and handler evidence. Live provider behavior and OAuth consent still require an operator-controlled integration test with an explicitly selected account.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "permittedRootIds": {
          "type": "string",
          "minLength": 1,
          "description": "Comma-separated permitted concrete folder IDs; maximum 20. Descendants require ancestry verification."
        },
        "sharedDriveId": {
          "description": "When set, every retrieved file must belong to this shared drive.",
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,200}$"
        }
      },
      "required": [
        "permittedRootIds"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "listFiles",
        "description": "List or search a single permitted folder's direct children, one bounded page at a time.",
        "parameters": [
          {
            "name": "folderId",
            "type": "string"
          },
          {
            "name": "nameContains",
            "type": "string",
            "optional": true
          },
          {
            "name": "pageSize",
            "type": "number",
            "optional": true
          },
          {
            "name": "pageToken",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<{ files: Record<string, unknown>[]; nextPageToken: string | null; incompleteSearch: boolean }>",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "readonly": true,
          "category": "google-drive",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "getFile",
        "description": "Read selected file metadata after verifying folder ancestry.",
        "parameters": [
          {
            "name": "fileId",
            "type": "string"
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "readonly": true,
          "category": "google-drive",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "readFile",
        "description": "Read up to 5000000 bytes of a permitted binary file as text or base64.",
        "parameters": [
          {
            "name": "fileId",
            "type": "string"
          }
        ],
        "returnType": "Promise<{ fileId: string; mimeType: string; byteLength: number; text?: string; base64?: string }>",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "readonly": true,
          "category": "google-drive",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "exportFile",
        "description": "Export a permitted Workspace document to a supported text or binary format, up to 5000000 bytes.",
        "parameters": [
          {
            "name": "fileId",
            "type": "string"
          },
          {
            "name": "mimeType",
            "type": "\"text/plain\" | \"text/csv\" | \"application/pdf\" | \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\" | \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\" | \"application/vnd.openxmlformats-officedocument.presentationml.presentation\""
          }
        ],
        "returnType": "Promise<{ fileId: string; mimeType: string; byteLength: number; text?: string; base64?: string }>",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "readonly": true,
          "category": "google-drive",
          "tags": [
            "readonly"
          ]
        }
      }
    ]
  },
  {
    "slug": "gmail",
    "version": "1.0.8",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/gmail/1.0.8/d0e7678bad284f0e3b407a491e84de68d5148dd7955a438174d40d69f896331d.mjs",
    "digest": "d0e7678bad284f0e3b407a491e84de68d5148dd7955a438174d40d69f896331d",
    "contract_version": 1,
    "display_name": "Gmail",
    "description": "Gmail integration (restricted scope). Requires a BYO Google OAuth client — first-party OAuth is not available for Gmail.",
    "category": "Communication",
    "auth": {
      "type": "oauth2",
      "provider": "google-oauth",
      "label": "Google (Gmail — BYO only)",
      "hint": "Connect your own Google OAuth application in OrgSDK. The host refreshes tokens; plugins receive only a run-scoped access token.",
      "scopes": [
        "https://mail.google.com/"
      ],
      "callbackPath": "/callback",
      "setupGuide": {
        "summary": "Gmail uses restricted Google scopes. Connect your own Google OAuth application in OrgSDK; the former one-click first-party relay is unavailable. The host owns consent and refresh, and plugins receive only a fresh access token.",
        "prerequisites": [
          "Access to create or edit a Google Cloud project.",
          "Permission to configure the project's OAuth consent screen and credentials.",
          "A Google account that can authorize the requested workspace data."
        ],
        "steps": [
          {
            "title": "Open or create a Google Cloud project",
            "description": "Use an existing project or create a new one to hold the OAuth client for Gmail.",
            "link": {
              "label": "Google Cloud Console",
              "url": "https://console.cloud.google.com/"
            }
          },
          {
            "title": "Enable the Gmail API",
            "description": "Gmail requires the Gmail API. Enable it in APIs & Services for this project.",
            "link": {
              "label": "Enable the Gmail API",
              "url": "https://console.cloud.google.com/apis/library/gmail.googleapis.com"
            }
          },
          {
            "title": "Configure the OAuth consent screen",
            "description": "Set the consent screen to External (or Internal for Workspace). Add your own Google account as a test user while the app remains in testing — Google's full verification process is separate from this inline flow."
          },
          {
            "title": "Create a Web application OAuth client",
            "description": "Create credentials of type OAuth client ID. The application type must be Web application — not Desktop app or another type."
          },
          {
            "title": "Add the authorized redirect URI",
            "description": "Register the exact redirect URI shown by OrgSDK: your registered workspace origin followed by /oauth/callback. Add it under Authorized redirect URIs, not JavaScript origins."
          },
          {
            "title": "Download the OAuth client JSON",
            "description": "Download the OAuth client configuration, then enter its client ID and client secret in OrgSDK’s native Connect form. Keep these secrets on the host; do not put them in plugin configuration."
          },
          {
            "title": "Connect Gmail",
            "description": "Saving valid JSON enables the authorization action. Complete Google's consent screen and return here — the connection refreshes in place."
          }
        ],
        "callback": {
          "label": "Authorized redirect URI",
          "instruction": "Paste this exact URL into the OAuth client's Authorized redirect URIs list."
        },
        "configField": {
          "label": "Google OAuth client JSON",
          "description": "Paste the complete contents of the JSON file downloaded for the Web application OAuth client. It contains a client ID and client secret.",
          "placeholder": "{ \"web\": { ... } }"
        },
        "permissions": {
          "https://mail.google.com/": {
            "label": "Read, compose, send, and manage your Gmail",
            "classification": "Restricted / broad"
          }
        },
        "troubleshootingUrl": "https://support.google.com/cloud/answer/6158849"
      }
    },
    "marketplace": {
      "displayName": "Gmail",
      "category": "Communication",
      "tagline": "Give agents a scoped Gmail inbox — search threads, extract attachments, draft replies, and send templated email on a schedule.",
      "tags": [
        "email",
        "google"
      ],
      "useCases": [
        "Triage a label by priority and surface threads that need a reply",
        "Draft and send replies that match the thread context",
        "Extract attachments and parse PDFs (e.g. invoices, expense reports)",
        "Route receipt threads to a Google Sheet and mark them processed",
        "Send templated notifications or reports with attachments"
      ],
      "permissionsSummary": "Read, modify labels on, and send Gmail messages via a restricted OAuth scope. Reading and labeling are reversible; sending and replying are mutating.",
      "links": {
        "homepage": "https://mail.google.com",
        "docs": "https://developers.google.com/gmail/api",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-gmail"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "08b0f9a1f71a23abd336ddf00256ed4386592354",
      "notices_url": "https://orgsdk.ai/plugins/packages/gmail/1.0.8/d0e7678bad284f0e3b407a491e84de68d5148dd7955a438174d40d69f896331d.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Gmail for OrgSDK\n\n## Install in OrgSDK\n\nOpen your project's Plugins page, select this plugin and enable the pinned release.\nThe default catalogue is https://orgsdk.ai/plugins/releases.json. The executor\ndownloads the versioned bundle and verifies its SHA256. Configure the provider\nand attach the credential described below before running an action.\n\nThe plugin is Apache-2.0 licensed; see LICENSE, NOTICE and TRADEMARKS.md.\nThe generated npm distribution is a separate publication surface. Installing npm\nfiles alone does not register a project plugin. Local builds do not publish it.\n\n## Credentials\n\nConnect your own Google OAuth application in OrgSDK with `https://mail.google.com/`.\nEnable the corresponding Google API and register the exact callback URL shown\nby OrgSDK in your Google Cloud OAuth client. Attach the resulting `google-oauth`\ncredential to this project plugin. The host owns consent and token refresh;\nexecution receives only `{source: \"byo\", secret_accessToken}`. Client and refresh\nsecrets stay on the host. No hosted OAuth relay is supported.\n\nA Google credential may be shared with another Google plugin only when its granted\nscopes cover both plugins. Provider consent, app verification and account permissions\nare required independently of installing this package.\n\nGmail uses a restricted scope. Distribution may require Google verification and a CASA assessment; publishing this plugin does not satisfy those requirements. Sending messages changes the recipient mailbox and needs appropriate approval.\n\n## Functions\n\n- `getMessages` — Fetch Gmail messages using Gmail search query syntax (q). Label filters must use the label name (not the label ID), e.g. label:Expense or label:\"Expense Reports\". Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.\n- `getMessage` — Fetch a single Gmail message by ID. Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.\n- `listLabels` — List Gmail labels (id + name)\n- `ensureLabel` — Get or create a Gmail label by name; returns label object (id + name)\n- `modifyMessageLabels` — Add/remove Gmail label IDs on a message. Use removeLabelIds:['UNREAD'] to mark as read.\n- `batchModifyMessages` — Batch add/remove label IDs across multiple message IDs\n- `markMessageAsRead` — Mark Gmail message as read (removes UNREAD label)\n- `markMessageAsUnread` — Mark Gmail message as unread (adds UNREAD label)\n- `sendMessage` — Send an email via Gmail. Supports plain-text or HTML body. When html is provided, sends a multipart/alternative message with both text/plain and text/html parts.\n- `sendMessageWithAttachment` — Send an email with file attachments via Gmail. Builds a MIME multipart/mixed message with a plain-text or HTML body and one or more base64-encoded attachments. When html is provided, the body is nested in multipart/alternative.\n- `replyToMessage` — Reply to a Gmail message within its thread. Fetches the original message to set threading headers (In-Reply-To, References) and replies to the original sender. Supports HTML via multipart/alternative when html parameter is provided.\n- `getMessageAttachment` — Fetch a Gmail attachment by messageId + attachmentId. Returns standard base64 bytes (dataBase64).\n- `getThread` — Fetch all messages in a Gmail thread by threadId, in chronological order. Unlike getMessages, this preserves quoted reply chains so the full conversation context is available. Use this before replying to understand the complete thread history.\n\n## Example action\n\n```typescript\nreturn await gmail.getMessages({ query: \"is:unread\", maxResults: 10 });\n```\n\nUse named argument objects. Exact parameters and return types are in the release catalogue and the project tool contract.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "getMessages",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch Gmail messages using Gmail search query syntax (q). Label filters must use the label name (not the label ID), e.g. label:Expense or label:\"Expense Reports\". Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.",
        "returnType": "Promise<Array<{ id: string; threadId?: string; subject?: string; from?: string; date?: string; body: string; headers?: Record<string, string>; labelIds?: string[]; attachments?: Array<{ filename: string; mimeType?: string; attachmentId: string; size?: number }> }>>",
        "parameters": [
          {
            "name": "query",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessage",
            "listLabels"
          ]
        },
        "docs": {
          "summary": "Fetch Gmail messages using Gmail search query syntax. Returns cleaned text bodies optimized for LLM processing — HTML is converted to markdown, tracking pixels and quoted reply chains are removed.",
          "parameters": [
            {
              "name": "query",
              "type": "string",
              "description": "Gmail search query syntax. Label filters must use label name (not ID), e.g. label:Expense. Supports standard operators: from:, to:, subject:, after:, before:, is:unread, has:attachment.",
              "example": "\"label:Expense is:unread after:2026/01/01\"",
              "optional": true
            },
            {
              "name": "maxResults",
              "type": "number",
              "description": "Maximum number of messages to return.",
              "defaultValue": "10",
              "optional": true,
              "example": "20"
            }
          ],
          "returns": {
            "type": "Promise<GmailMessage[]>",
            "description": "Array of message objects with id, subject, from, date, body (cleaned text), headers, labelIds, and attachments.",
            "example": "[{ id: \"18f...\", subject: \"Invoice #42\", from: \"vendor@example.com\", body: \"Please find attached...\", attachments: [{ filename: \"invoice.pdf\", attachmentId: \"ANGj...\" }] }]"
          },
          "examples": [
            {
              "title": "Fetch recent unread emails",
              "code": "const msgs = await gmail.getMessages(\"is:unread\", 5);\nmsgs.map(m => ({ subject: m.subject, from: m.from }));"
            },
            {
              "title": "Search by label and date",
              "code": "const invoices = await gmail.getMessages(\"label:Invoices after:2026/01/01\", 50);"
            },
            {
              "title": "Filter with LLM classification",
              "code": "const msgs = await gmail.getMessages(\"label:inbox\", 20);\nconst expenses = [];\nfor (const m of msgs) {\n  const isExpense = await llm.evalBoolean(\n    m.body,\n    \"is an expense receipt or invoice\",\n    { text: \"Your payment of $50 was processed\", matches: true },\n    { text: \"Meeting tomorrow at 3pm\", matches: false }\n  );\n  if (isExpense.result) expenses.push(m);\n}\nexpenses;",
              "description": "Combine with llm.evalBoolean to classify emails"
            }
          ],
          "caveats": [
            "Use label names (not IDs) in query filters — e.g. label:Expense, not label:Label_13",
            "Messages are fetched sequentially; limit maxResults to 20-50 to stay within timeout",
            "Quoted reply chains are truncated — only the first/main message body is returned"
          ],
          "seeAlso": [
            "getMessage",
            "listLabels",
            "modifyMessageLabels"
          ]
        }
      },
      {
        "name": "getMessage",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch a single Gmail message by ID. Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.",
        "returnType": "Promise<{ id: string; threadId?: string; subject?: string; from?: string; date?: string; body: string; headers?: Record<string, string>; labelIds?: string[]; attachments?: Array<{ filename: string; mimeType?: string; attachmentId: string; size?: number }> }>",
        "parameters": [
          {
            "name": "id",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessages",
            "getMessageAttachment"
          ]
        }
      },
      {
        "name": "listLabels",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "List Gmail labels (id + name)",
        "returnType": "Promise<Array<{ id?: string; name?: string }>>",
        "parameters": [],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "ensureLabel",
            "getMessages"
          ]
        }
      },
      {
        "name": "ensureLabel",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Get or create a Gmail label by name; returns label object (id + name)",
        "returnType": "Promise<{ id?: string; name?: string }>",
        "parameters": [
          {
            "name": "name",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listLabels",
            "modifyMessageLabels"
          ]
        }
      },
      {
        "name": "modifyMessageLabels",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Add/remove Gmail label IDs on a message. Use removeLabelIds:['UNREAD'] to mark as read.",
        "returnType": "Promise<{ success: boolean; id?: string; labelIds: string[] }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          },
          {
            "name": "addLabelIds",
            "type": "string[]"
          },
          {
            "name": "removeLabelIds",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "ensureLabel",
            "markMessageAsRead",
            "markMessageAsUnread"
          ]
        }
      },
      {
        "name": "batchModifyMessages",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Batch add/remove label IDs across multiple message IDs",
        "returnType": "Promise<{ success: boolean; count: number }>",
        "parameters": [
          {
            "name": "messageIds",
            "type": "string[]"
          },
          {
            "name": "addLabelIds",
            "type": "string[]"
          },
          {
            "name": "removeLabelIds",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "modifyMessageLabels"
          ]
        }
      },
      {
        "name": "markMessageAsRead",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark Gmail message as read (removes UNREAD label)",
        "returnType": "Promise<{ success: boolean; id?: string; labelIds: string[] }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "modifyMessageLabels",
            "markMessageAsUnread"
          ]
        }
      },
      {
        "name": "markMessageAsUnread",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark Gmail message as unread (adds UNREAD label)",
        "returnType": "Promise<{ success: boolean; id?: string; labelIds: string[] }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "modifyMessageLabels",
            "markMessageAsRead"
          ]
        }
      },
      {
        "name": "sendMessage",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Send an email via Gmail. Supports plain-text or HTML body. When html is provided, sends a multipart/alternative message with both text/plain and text/html parts.",
        "returnType": "Promise<{ success: boolean; id: string; threadId?: string }>",
        "parameters": [
          {
            "name": "to",
            "type": "string"
          },
          {
            "name": "subject",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string"
          },
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "cc",
            "type": "string"
          },
          {
            "name": "bcc",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.1.0",
          "relatedTo": [
            "getMessages",
            "replyToMessage",
            "sendMessageWithAttachment"
          ]
        }
      },
      {
        "name": "sendMessageWithAttachment",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Send an email with file attachments via Gmail. Builds a MIME multipart/mixed message with a plain-text or HTML body and one or more base64-encoded attachments. When html is provided, the body is nested in multipart/alternative.",
        "returnType": "Promise<{ success: boolean; id: string; threadId?: string }>",
        "parameters": [
          {
            "name": "to",
            "type": "string"
          },
          {
            "name": "subject",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string"
          },
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "attachments",
            "type": "Array<{ filename: string; mimeType: string; base64Data: string }>"
          },
          {
            "name": "cc",
            "type": "string"
          },
          {
            "name": "bcc",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.1.0",
          "relatedTo": [
            "sendMessage",
            "generatePdf"
          ]
        },
        "docs": {
          "summary": "Send an email with file attachments via Gmail. Constructs a MIME multipart/mixed message with a text body and base64-encoded attachment parts. Useful for sending generated PDFs, reports, or other files.",
          "parameters": [
            {
              "name": "to",
              "type": "string",
              "description": "Recipient email address",
              "example": "\"client@example.com\""
            },
            {
              "name": "subject",
              "type": "string",
              "description": "Email subject line",
              "example": "\"Invoice #1001\""
            },
            {
              "name": "body",
              "type": "string",
              "description": "Plain-text email body (fallback for clients that don't support HTML)",
              "example": "\"Please find your invoice attached.\""
            },
            {
              "name": "html",
              "type": "string",
              "description": "Optional HTML email body. When provided, sends multipart/alternative with both plain text and HTML.",
              "optional": true
            },
            {
              "name": "attachments",
              "type": "Array<{ filename: string; mimeType: string; base64Data: string }>",
              "description": "Array of attachments. Each needs a filename, MIME type, and base64-encoded data.",
              "example": "[{ filename: \"invoice.pdf\", mimeType: \"application/pdf\", base64Data: \"JVBERi0xLjQK...\" }]"
            },
            {
              "name": "cc",
              "type": "string",
              "description": "CC recipients (optional)"
            },
            {
              "name": "bcc",
              "type": "string",
              "description": "BCC recipients (optional)"
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean; id: string; threadId?: string }>",
            "description": "Confirmation with the sent message ID and optional thread ID.",
            "example": "{ success: true, id: \"18e3a...\", threadId: \"18e3a...\" }"
          },
          "examples": [
            {
              "title": "Generate a PDF and email it",
              "code": "const pdf = await browser.generatePdf(\"<html><body><h1>Invoice</h1></body></html>\", { filename: \"invoice.pdf\" });\nawait gmail.sendMessageWithAttachment(\n  \"client@example.com\",\n  \"Your Invoice\",\n  \"Please find your invoice attached.\",\n  [{ filename: pdf.filename, mimeType: \"application/pdf\", base64Data: pdf.base64 }]\n);",
              "description": "Generate a PDF invoice and send it as an email attachment"
            }
          ],
          "seeAlso": [
            "gmail.sendMessage",
            "browser.generatePdf",
            "gmail.replyToMessage"
          ]
        }
      },
      {
        "name": "replyToMessage",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Reply to a Gmail message within its thread. Fetches the original message to set threading headers (In-Reply-To, References) and replies to the original sender. Supports HTML via multipart/alternative when html parameter is provided.",
        "returnType": "Promise<{ success: boolean; id: string; threadId?: string }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string"
          },
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "cc",
            "type": "string"
          },
          {
            "name": "bcc",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.1.0",
          "relatedTo": [
            "sendMessage",
            "getMessage"
          ]
        }
      },
      {
        "name": "getMessageAttachment",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch a Gmail attachment by messageId + attachmentId. Returns standard base64 bytes (dataBase64).",
        "returnType": "Promise<{ messageId: string; attachmentId: string; size?: number; dataBase64: string }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          },
          {
            "name": "attachmentId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessage",
            "parsePdf"
          ]
        }
      },
      {
        "name": "getThread",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch all messages in a Gmail thread by threadId, in chronological order. Unlike getMessages, this preserves quoted reply chains so the full conversation context is available. Use this before replying to understand the complete thread history.",
        "returnType": "Promise<Array<{ id: string; subject?: string; from?: string; to?: string; date?: string; body: string; threadId?: string; labelIds?: string[] }>>",
        "parameters": [
          {
            "name": "threadId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessage",
            "getMessages",
            "replyToMessage"
          ]
        },
        "docs": {
          "summary": "Fetch all messages in a Gmail thread in chronological order. Preserves quoted reply content so the full conversation history is visible. Essential context before generating a reply.",
          "parameters": [
            {
              "name": "threadId",
              "type": "string",
              "description": "The Gmail thread ID (available as threadId on any GmailMessage object)",
              "example": "\"18f3a...\""
            }
          ],
          "returns": {
            "type": "Promise<GmailMessage[]>",
            "description": "Chronological array of all messages in the thread.",
            "example": "[{ id: \"18f...\", from: \"alice@example.com\", subject: \"Re: Project\", body: \"...\" }, ...]"
          },
          "examples": [
            {
              "title": "Get full thread before replying",
              "code": "const msgs = await gmail.getMessages(\"is:unread\", 5);\nfor (const msg of msgs) {\n  const thread = await gmail.getThread(msg.threadId);\n  const history = thread.map(m => `[${m.from}]: ${m.body}`).join(\"\\n\\n---\\n\\n\");\n  // pass history to agent for context\n}"
            }
          ],
          "seeAlso": [
            "getMessage",
            "getMessages",
            "replyToMessage"
          ]
        }
      }
    ]
  },
  {
    "slug": "github",
    "version": "1.0.3",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/github/1.0.3/2cadd809a29176ed7fb0bd52bd118b95c6a08b7deee8a67afed24d82f81ff70a.mjs",
    "digest": "2cadd809a29176ed7fb0bd52bd118b95c6a08b7deee8a67afed24d82f81ff70a",
    "contract_version": 1,
    "display_name": "GitHub",
    "description": "GitHub repository operations: issues, PRs, branches, Actions, releases",
    "category": "Developer",
    "auth": {
      "type": "token",
      "provider": "github",
      "label": "GitHub personal access token",
      "hint": "Create a classic token at github.com/settings/tokens — grant the repo scope (add workflow if you touch Actions)."
    },
    "marketplace": {
      "displayName": "GitHub",
      "category": "Developer",
      "tagline": "Put your repositories in the hands of a persistent agent team — triage issues, review and merge PRs, react to failed CI, and cut releases on a schedule.",
      "tags": [
        "devops",
        "git"
      ],
      "useCases": [
        "Auto-triage and label incoming issues, then draft responses for review",
        "Review pull requests: pull the diff, summarize the risk, and request reviewers",
        "React to failed CI runs — pull job logs and re-run only the failed jobs",
        "Draft releases with a generated changelog from merged PRs",
        "Keep a branch current and open a pull request from a generated change"
      ],
      "permissionsSummary": "Read and write GitHub issues, pull requests, branches, files, Actions, and releases. Reads are non-mutating; writes and merges are mutating.",
      "links": {
        "homepage": "https://github.com",
        "docs": "https://docs.github.com/en/rest",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-github"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "588d275d70beae9cf0e5bd5a3c866a0dfc09da13",
      "notices_url": "https://orgsdk.ai/plugins/packages/github/1.0.3/2cadd809a29176ed7fb0bd52bd118b95c6a08b7deee8a67afed24d82f81ff70a.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# GitHub\n\nRead and write GitHub repositories from agent workflows — issues, pull\nrequests, branches, releases, file contents, and Actions workflow runs.\n\n## What it does\n\nThe `github` plugin exposes GitHub repository operations as safe functions.\nOnce the plugin is enabled on an agent, those functions become globals inside\nthe sandbox — callable from any workflow with no imports.\n\nTypical uses:\n\n- **Issue triage** — open, label, and comment on issues; classify inbound\n  feedback with the `llm` plugin, then file the result as a GitHub issue.\n- **Pull-request automation** — open PRs from generated branches and post\n  review comments.\n- **Release + changelog** — cut a release and write release notes generated by\n  an LLM.\n- **CI visibility** — list workflow runs, fetch artefacts, and surface failed\n  job logs back into a chat session.\n\n## Function groups\n\n| Group | Examples |\n|---|---|\n| Issues | `createIssue`, `getIssue`, `addIssueComment`, `addIssueLabels`, `listIssueComments` |\n| Pull requests | `createPullRequest`, `getPullRequest`, `addPrComment` |\n| Branches | `createBranch`, `getBranch`, `listBranches` |\n| Files / content | `getFileContent`, `createOrUpdateFile`, `listDirectoryContents` |\n| Releases | `createRelease`, `getRelease` |\n| Workflows / Actions | `getWorkflowRun`, `cancelWorkflowRun`, `downloadWorkflowArtifact`, `getWorkflowJobLogs` |\n| Repository | `getRepo`, `getRepoContributors` |\n\nThe full function list (parameters, return types, credential needs) is\navailable per-plugin via the `plugins:info` command in the dashboard.\n\n## Credentials\n\nThis plugin needs a **GitHub personal access token** (the `github`\ncredential provider). Every function declares\n`requiredCredentials: [\"github\"]`, so none of its functions are callable\nuntil a credential is connected.\n\n| Provider | Type | How to get it |\n|---|---|---|\n| `github` | token | Create a classic token at `github.com/settings/tokens`. Grant the **`repo`** scope (add **`workflow`** if you touch Actions files). |\n\nConnect it inline from the plugin row (the \"set credentials\" affordance wires\nto the standard token credential flow), or via the CLI:\n\n```bash\norgsdk credential set github default ghp_your_token_here\n```\n\n## Using it in a workflow\n\n```typescript\n// Open an issue from structured data\nconst issue = await createIssue({\n  owner: \"myorg\",\n  repo: \"myrepo\",\n  title: \"Bug: checkout fails on Safari\",\n  body: \"Steps to reproduce...\",\n  labels: [\"bug\", \"frontend\"],\n});\n\n// Comment on an existing PR\nawait addPrComment({\n  owner: \"myorg\",\n  repo: \"myrepo\",\n  pullNumber: 42,\n  body: \"Deployed to staging ✅\",\n});\n```\n\n## Notes\n\n- All functions take an `{ owner, repo, ... }` options object — the repository\n  is addressed per-call, not configured globally.\n- Read operations (`get*`, `list*`) are tagged `readonly` and safe to use in\n  discovery workflows.\n- This plugin has no `guides/` reference yet; agent usage patterns can be added\n  there later (agent-facing) without touching this human-facing doc.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "listWorkflowRuns",
        "requiredCredentials": [
          "github"
        ],
        "description": "List workflow runs for a repository. Optionally filter by actionId (filename or ID), branch, status ('queued', 'in_progress', 'completed').",
        "returnType": "Promise<Array<{ id: number; name: string | null; status: string | null; conclusion: string | null; headBranch: string | null; headSha: string; event: string; htmlUrl: string; runNumber: number; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "actionId",
            "type": "string | number",
            "optional": true
          },
          {
            "name": "branch",
            "type": "string",
            "optional": true
          },
          {
            "name": "status",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getWorkflowRun",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get details of a specific workflow run by ID.",
        "returnType": "Promise<{ id: number; name: string | null; status: string | null; conclusion: string | null; headBranch: string | null; headSha: string; event: string; htmlUrl: string; runNumber: number; createdAt: string; updatedAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "triggerWorkflow",
        "requiredCredentials": [
          "github"
        ],
        "description": "Trigger a workflow_dispatch event. actionId can be the workflow filename (e.g. 'ci.yml') or numeric ID. ref is the branch/tag to run on.",
        "returnType": "Promise<{ triggered: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "actionId",
            "type": "string | number"
          },
          {
            "name": "ref",
            "type": "string"
          },
          {
            "name": "inputs",
            "type": "Record<string, string>",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "rerunWorkflowFailedJobs",
        "requiredCredentials": [
          "github"
        ],
        "description": "Re-run only the failed jobs of a workflow run. Returns immediately — poll getWorkflowRun to track progress.",
        "returnType": "Promise<{ rerunned: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "rerunWorkflow",
        "requiredCredentials": [
          "github"
        ],
        "description": "Re-run all jobs in a workflow run. Returns immediately — poll getWorkflowRun to track progress.",
        "returnType": "Promise<{ rerunned: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "cancelWorkflowRun",
        "requiredCredentials": [
          "github"
        ],
        "description": "Cancel an in-progress workflow run.",
        "returnType": "Promise<{ cancelled: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listWorkflowJobs",
        "requiredCredentials": [
          "github"
        ],
        "description": "List jobs for a workflow run. Includes step-level detail so you can see which step failed. filter can be 'latest' (default) or 'all' (includes previous attempts).",
        "returnType": "Promise<Array<{ id: number; runId: number; name: string; status: string; conclusion: string | null; startedAt: string | null; completedAt: string | null; htmlUrl: string; runnerName: string | null; steps: Array<{ name: string; status: string; conclusion: string | null; number: number }> }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          },
          {
            "name": "filter",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getWorkflowJobLogs",
        "requiredCredentials": [
          "github"
        ],
        "description": "Download logs for a specific job. Output is capped at 100KB. Use listWorkflowJobs first to get the jobId.",
        "returnType": "Promise<{ logs: string; truncated: boolean }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "jobId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "listWorkflowRunArtifacts",
        "requiredCredentials": [
          "github"
        ],
        "description": "List artifacts produced by a workflow run.",
        "returnType": "Promise<Array<{ id: number; name: string; sizeInBytes: number; expired: boolean; expiresAt: string | null; createdAt: string | null; updatedAt: string | null }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "downloadWorkflowArtifact",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a temporary download URL for a workflow artifact (zip). The URL is short-lived. Use listWorkflowRunArtifacts first to get the artifactId.",
        "returnType": "Promise<{ downloadUrl: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "artifactId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "listBranches",
        "requiredCredentials": [
          "github"
        ],
        "description": "List branches for a GitHub repository.",
        "returnType": "Promise<Array<{ name: string; sha: string; protected: boolean }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getBranch",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get details of a specific branch including latest commit info.",
        "returnType": "Promise<{ name: string; sha: string; protected: boolean; commitMessage: string; commitAuthor: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "branch",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createBranch",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new branch. fromRef defaults to the repo's default branch. Example: createBranch('owner', 'repo', 'feature/new', 'main').",
        "returnType": "Promise<{ ref: string; sha: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "branchName",
            "type": "string"
          },
          {
            "name": "fromRef",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "getFileContent",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get the content of a file from a GitHub repository. Auto-decodes base64 to UTF-8. Errors on files >1MB. Use ref to specify a branch/tag/commit.",
        "returnType": "Promise<{ name: string; path: string; sha: string; size: number; content: string; htmlUrl: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "path",
            "type": "string"
          },
          {
            "name": "ref",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createOrUpdateFile",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create or update a file in a GitHub repository. Provide sha to update an existing file (get it from getFileContent). Content is auto-encoded to base64.",
        "returnType": "Promise<{ path: string; sha: string; htmlUrl: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "path",
            "type": "string"
          },
          {
            "name": "content",
            "type": "string"
          },
          {
            "name": "message",
            "type": "string"
          },
          {
            "name": "branch",
            "type": "string",
            "optional": true
          },
          {
            "name": "sha",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listDirectoryContents",
        "requiredCredentials": [
          "github"
        ],
        "description": "List contents of a directory in a GitHub repository. Defaults to root. Use ref to specify a branch/tag/commit.",
        "returnType": "Promise<Array<{ name: string; path: string; type: 'file' | 'dir' | 'submodule' | 'symlink'; sha: string; size: number; htmlUrl: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "path",
            "type": "string",
            "optional": true
          },
          {
            "name": "ref",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "listIssues",
        "requiredCredentials": [
          "github"
        ],
        "description": "List issues for a GitHub repository. Filters out pull requests. State: 'open', 'closed', 'all'. Labels: comma-separated string.",
        "returnType": "Promise<Array<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "state",
            "type": "string",
            "optional": true
          },
          {
            "name": "labels",
            "type": "string",
            "optional": true
          },
          {
            "name": "assignee",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getIssue",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a single GitHub issue by number.",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createIssue",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new GitHub issue.",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "title",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "labels",
            "type": "string[]",
            "optional": true
          },
          {
            "name": "assignees",
            "type": "string[]",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "updateIssue",
        "requiredCredentials": [
          "github"
        ],
        "description": "Update a GitHub issue. Only provided fields are changed. State: 'open' or 'closed'.",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "title",
            "type": "string",
            "optional": true
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "state",
            "type": "string",
            "optional": true
          },
          {
            "name": "labels",
            "type": "string[]",
            "optional": true
          },
          {
            "name": "assignees",
            "type": "string[]",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "addIssueComment",
        "requiredCredentials": [
          "github"
        ],
        "description": "Add a comment to a GitHub issue.",
        "returnType": "Promise<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "body",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listIssueComments",
        "requiredCredentials": [
          "github"
        ],
        "description": "List comments on a GitHub issue.",
        "returnType": "Promise<Array<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "addIssueLabels",
        "requiredCredentials": [
          "github"
        ],
        "description": "Add labels to a GitHub issue. Returns the full list of labels on the issue.",
        "returnType": "Promise<string[]>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "labels",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listPullRequests",
        "requiredCredentials": [
          "github"
        ],
        "description": "List pull requests for a GitHub repository. State: 'open', 'closed', 'all'. head/base filter by branch name.",
        "returnType": "Promise<Array<{ id: number; number: number; title: string; body: string | null; state: string; draft: boolean; merged: boolean; head: { ref: string; sha: string }; base: { ref: string; sha: string }; user: string | null; labels: string[]; htmlUrl: string; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "state",
            "type": "string",
            "optional": true
          },
          {
            "name": "head",
            "type": "string",
            "optional": true
          },
          {
            "name": "base",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getPullRequest",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a single pull request by number. Includes diff stats (additions, deletions, changedFiles).",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; draft: boolean; merged: boolean; head: { ref: string; sha: string }; base: { ref: string; sha: string }; user: string | null; labels: string[]; htmlUrl: string; additions: number; deletions: number; changedFiles: number; createdAt: string; updatedAt: string; mergedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createPullRequest",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new pull request. head = source branch, base = target branch (e.g. 'main').",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; draft: boolean; head: { ref: string; sha: string }; base: { ref: string; sha: string }; htmlUrl: string; createdAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "title",
            "type": "string"
          },
          {
            "name": "head",
            "type": "string"
          },
          {
            "name": "base",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "draft",
            "type": "boolean",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "addPrComment",
        "requiredCredentials": [
          "github"
        ],
        "description": "Add a comment to a pull request.",
        "returnType": "Promise<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "body",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listPrComments",
        "requiredCredentials": [
          "github"
        ],
        "description": "List comments on a pull request.",
        "returnType": "Promise<Array<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "mergePullRequest",
        "requiredCredentials": [
          "github"
        ],
        "description": "Merge a pull request. mergeMethod: 'merge', 'squash', or 'rebase'. Defaults to 'merge'.",
        "returnType": "Promise<{ merged: boolean; message: string; sha: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "mergeMethod",
            "type": "string",
            "optional": true
          },
          {
            "name": "commitTitle",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "requestPrReview",
        "requiredCredentials": [
          "github"
        ],
        "description": "Request reviewers for a pull request.",
        "returnType": "Promise<{ requestedReviewers: string[] }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "reviewers",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listReleases",
        "requiredCredentials": [
          "github"
        ],
        "description": "List releases for a GitHub repository, ordered by creation date (newest first).",
        "returnType": "Promise<Array<{ id: number; tagName: string; name: string | null; body: string | null; draft: boolean; prerelease: boolean; htmlUrl: string; createdAt: string; publishedAt: string | null; assets: Array<{ name: string; size: number; downloadUrl: string }> }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getRelease",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a specific release by ID.",
        "returnType": "Promise<{ id: number; tagName: string; name: string | null; body: string | null; draft: boolean; prerelease: boolean; htmlUrl: string; createdAt: string; publishedAt: string | null; assets: Array<{ name: string; size: number; downloadUrl: string }> }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "releaseId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createRelease",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new GitHub release. tagName is required. targetCommitish defaults to the default branch.",
        "returnType": "Promise<{ id: number; tagName: string; name: string | null; body: string | null; draft: boolean; prerelease: boolean; htmlUrl: string; createdAt: string; publishedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "tagName",
            "type": "string"
          },
          {
            "name": "name",
            "type": "string",
            "optional": true
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "draft",
            "type": "boolean",
            "optional": true
          },
          {
            "name": "prerelease",
            "type": "boolean",
            "optional": true
          },
          {
            "name": "targetCommitish",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listRepos",
        "requiredCredentials": [
          "github"
        ],
        "description": "List GitHub repositories for a user or the authenticated user. Returns simplified repo objects.",
        "returnType": "Promise<Array<{ id: number; name: string; fullName: string; description: string | null; private: boolean; defaultBranch: string; language: string | null; stargazersCount: number; forksCount: number; openIssuesCount: number; htmlUrl: string; createdAt: string | null; updatedAt: string | null }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string",
            "optional": true
          },
          {
            "name": "type",
            "type": "string",
            "optional": true
          },
          {
            "name": "sort",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getRepo",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get details of a specific GitHub repository.",
        "returnType": "Promise<{ id: number; name: string; fullName: string; description: string | null; private: boolean; defaultBranch: string; language: string | null; stargazersCount: number; forksCount: number; openIssuesCount: number; htmlUrl: string; createdAt: string | null; updatedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "searchRepos",
        "requiredCredentials": [
          "github"
        ],
        "description": "Search GitHub repositories using GitHub search query syntax. Example: 'language:typescript stars:>100'.",
        "returnType": "Promise<Array<{ id: number; name: string; fullName: string; description: string | null; private: boolean; defaultBranch: string; language: string | null; stargazersCount: number; forksCount: number; openIssuesCount: number; htmlUrl: string; createdAt: string | null; updatedAt: string | null }>>",
        "parameters": [
          {
            "name": "query",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getRepoContributors",
        "requiredCredentials": [
          "github"
        ],
        "description": "List contributors for a GitHub repository.",
        "returnType": "Promise<Array<{ login: string; contributions: number; htmlUrl: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      }
    ]
  },
  {
    "slug": "facebook",
    "version": "1.0.3",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/facebook/1.0.3/0d20cfb593953818967e950d46ab6d5a9012887f19c9048c55a4d3251d5d91b8.mjs",
    "digest": "0d20cfb593953818967e950d46ab6d5a9012887f19c9048c55a4d3251d5d91b8",
    "contract_version": 1,
    "display_name": "Facebook",
    "description": "Facebook page management: posting, scheduling, insights, and engagement metrics",
    "category": "Social",
    "auth": {
      "type": "oauth2",
      "provider": "facebook-oauth",
      "label": "Facebook (Pages) OAuth",
      "hint": "Create a Facebook OAuth app at developers.facebook.com; connect via the Credentials flow. Scopes cover page list, read, and post/engagement management.",
      "scopes": [
        "pages_show_list",
        "pages_read_engagement",
        "pages_read_user_content",
        "pages_manage_posts",
        "pages_manage_engagement"
      ],
      "callbackPath": "/callback"
    },
    "marketplace": {
      "displayName": "Facebook",
      "category": "Social",
      "tagline": "Manage Facebook Pages from agent workflows — posting, scheduling, insights, and engagement metrics.",
      "tags": [
        "social",
        "posting",
        "analytics"
      ],
      "useCases": [
        "Schedule page posts with link attachments",
        "Report on post insights and reach",
        "List and manage pages you admin",
        "Track engagement across recent posts"
      ],
      "permissionsSummary": "Post, delete, and read insights for Facebook Pages via OAuth. Posting and deleting are public, mutating actions; deletes are irreversible.",
      "links": {
        "homepage": "https://www.facebook.com",
        "docs": "https://developers.facebook.com/docs/pages",
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-facebook"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "0ddd8823f0a207b10cc8e48e6f1b1a6202cbd3cd",
      "notices_url": "https://orgsdk.ai/plugins/packages/facebook/1.0.3/0d20cfb593953818967e950d46ab6d5a9012887f19c9048c55a4d3251d5d91b8.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Current execution contract\n\nSet plugin configuration `{ \"pageId\": \"YOUR_PAGE_ID\" }`. The plugin resolves a page access token from the host-supplied user token, checks the returned page ID, and uses bearer headers. Do not store a page token or page ID in the OAuth snapshot.\n\nNative OAuth providers receive only `{source: \"byo\", secret_accessToken}`. The host owns refresh. The first-party relay is retired.\n\n## Historical source reference\n\nThe following describes the former author-source host; current npm and native credential behaviour above takes precedence.\n\n# Facebook\n\nPost to and read from Facebook Pages — publishing, scheduling, and engagement\ninsights. OAuth2 (BYO client).\n\n## What it does\n\nThe `facebook` plugin automates Facebook Page management: list your pages,\npublish posts (immediate or scheduled), fetch posts, pull engagement insights,\nand delete posts. It targets **Pages** (not personal profiles), which is the\ncorrect surface for brand/agency automation.\n\nTypical uses:\n\n- **Cross-posting** — publish the same content across Facebook, LinkedIn, and\n  X from one workflow (compose with the `linkedin` and `twitter` plugins).\n- **Scheduled campaigns** — queue a series of posts across a launch window.\n- **Engagement reporting** — pull per-post metrics into a sheet for review.\n\n## Functions\n\n| Function | Purpose |\n|---|---|\n| `getPages` | List Pages the connected account manages. |\n| `getPage` | Get a single Page. |\n| `getPagePosts` | List posts on a Page. |\n| `createPagePost` | Publish a post (immediate or `scheduledPublishTime`). |\n| `deletePagePost` | Delete a post. |\n| `getPostInsights` | Pull engagement metrics for a post. |\n\nThe full function list (parameters, return types) is available via the\n`plugins:info` command in the dashboard.\n\n## Credentials\n\nEvery function declares `requiredCredentials: [\"facebook-oauth\"]`. A Facebook\nOAuth credential must be connected before any function is callable.\n\n| Provider | Type | Connect method |\n|---|---|---|\n| `facebook-oauth` | oauth2 | BYO Facebook OAuth app. Connect via the dashboard Credentials flow; the granted scopes cover page list, read, and post/engagement management. |\n\nRequired scopes: `pages_show_list`, `pages_read_engagement`,\n`pages_read_user_content`, `pages_manage_posts`, `pages_manage_engagement`.\n\n## Using it in a workflow\n\n```typescript\nconst pages = await getPages();\nconst page = pages.find((p) => p.name === \"Acme\");\nif (!page) throw new Error(\"Acme page not found\");\n\nconst post = await createPagePost({\n  pageId: page.id,\n  message: \"New blog post: Scaling agent workflows →\",\n  linkUrl: \"https://acme.com/blog/scaling-agents\",\n});\n\n// Pull metrics later\nconst insights = await getPostInsights({ pageId: page.id, postId: post.id });\n```\n\n## Notes\n\n- All operations are scoped to a **Page** (`pageId`) — pass it per call.\n- Scheduled publishing uses `scheduledPublishTime` (UTC ISO string) on\n  `createPagePost`.\n- No `guides/` reference yet; agent usage patterns can be added later.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "pageId": {
          "description": "Facebook page ID. Required for page operations; getPages lists accessible pages.",
          "type": "string",
          "pattern": "^[a-zA-Z0-9_-]{1,100}$"
        }
      },
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "getPage",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Get the Facebook business page info including name, link, and follower count.",
        "returnType": "Promise<{ id: string; name: string; link: string; fanCount?: number }>",
        "parameters": [],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve the Facebook business page info for the configured page ID. Returns name, link, and follower count.",
          "parameters": [],
          "returns": {
            "type": "Promise<FacebookPage>",
            "description": "The Facebook page details."
          }
        }
      },
      {
        "name": "getPages",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "List all Facebook business pages where the authenticated user has admin access.",
        "returnType": "Promise<Array<{ id: string; name: string; link: string; fanCount?: number }>>",
        "parameters": [],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "List all Facebook business pages where the authenticated user is an admin. Returns page IDs needed for posting.",
          "parameters": [],
          "returns": {
            "type": "Promise<FacebookPage[]>",
            "description": "Array of pages with ID, name, link, and follower count."
          }
        }
      },
      {
        "name": "createPagePost",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Create a post on the Facebook business page. Supports link attachments and scheduled publishing.",
        "returnType": "Promise<{ id: string; url: string }>",
        "parameters": [
          {
            "name": "message",
            "type": "string"
          },
          {
            "name": "linkUrl",
            "type": "string",
            "optional": true
          },
          {
            "name": "scheduledPublishTime",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Publish a post on the Facebook business page. Supports link attachments and scheduled publishing. The post appears on the page timeline.",
          "parameters": [
            {
              "name": "message",
              "type": "string",
              "description": "The post message text.",
              "example": "\"Check out our new blog post!\""
            },
            {
              "name": "linkUrl",
              "type": "string",
              "description": "URL to attach as a link preview. Facebook will generate a preview card from Open Graph metadata.",
              "optional": true,
              "example": "\"https://quokly.com.au/blog/my-post\""
            },
            {
              "name": "scheduledPublishTime",
              "type": "number",
              "description": "Unix timestamp to schedule the post for future publication. Omit to publish immediately.",
              "optional": true,
              "example": "1715700000"
            }
          ],
          "returns": {
            "type": "Promise<{ id: string; url: string }>",
            "description": "The created post ID and Facebook URL."
          },
          "examples": [
            {
              "title": "Share a blog post link",
              "code": "const post = await facebook.createPagePost({\n  message: \"New on the blog: Why source transparency matters in AI research\",\n  linkUrl: \"https://quokly.com.au/blog/why-source-transparency\",\n});"
            },
            {
              "title": "Schedule a post for tomorrow",
              "code": "const tomorrow = Math.floor(Date.now() / 1000) + 86400;\nawait facebook.createPagePost({\n  message: \"Coming tomorrow: AI research for policy analysts\",\n  scheduledPublishTime: tomorrow,\n});"
            }
          ]
        }
      },
      {
        "name": "getPagePosts",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "List recent posts on the Facebook business page with engagement metrics.",
        "returnType": "Promise<Array<{ id: string; message?: string; createdTime: string; permalinkUrl: string; likes: number; comments: number; shares: number }>>",
        "parameters": [
          {
            "name": "limit",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "List recent posts on the Facebook business page with engagement metrics (likes, comments, shares).",
          "parameters": [
            {
              "name": "limit",
              "type": "number",
              "description": "Max number of posts to return. Default 10.",
              "optional": true,
              "defaultValue": "10"
            }
          ],
          "returns": {
            "type": "Promise<FacebookPagePost[]>",
            "description": "Array of page posts with engagement metrics."
          }
        }
      },
      {
        "name": "getPostInsights",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Get detailed insights for a Facebook page post including impressions, reach, engagement, and click metrics.",
        "returnType": "Promise<{ impressions: number; reach: number; likes: number; comments: number; shares: number; linkClicks: number; engagementRate: number }>",
        "parameters": [
          {
            "name": "postId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly",
            "analytics"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve detailed analytics for a Facebook page post. Returns impressions, reach, likes, comments, shares, link clicks, and engagement rate.",
          "parameters": [
            {
              "name": "postId",
              "type": "string",
              "description": "The Facebook post ID."
            }
          ],
          "returns": {
            "type": "Promise<FacebookPostInsights>",
            "description": "Detailed engagement and reach metrics."
          }
        }
      },
      {
        "name": "deletePagePost",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Delete a Facebook page post. This action is irreversible.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "postId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "write",
            "destructive"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Delete a Facebook page post. This action is irreversible.",
          "parameters": [
            {
              "name": "postId",
              "type": "string",
              "description": "The ID of the post to delete."
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean }>",
            "description": "Confirmation of deletion."
          }
        }
      }
    ]
  },
  {
    "slug": "data",
    "version": "1.0.3",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/data/1.0.3/caef4b5ce808d7d80e8f428971daaa9474261377cc24b7593a5c43b298d789f1.mjs",
    "digest": "caef4b5ce808d7d80e8f428971daaa9474261377cc24b7593a5c43b298d789f1",
    "contract_version": 1,
    "display_name": "Data Tools",
    "description": "Data utilities: HTTP fetching, array processing, transformation, currency conversion, and PDF extraction",
    "category": "Data & AI",
    "auth": {},
    "marketplace": {
      "displayName": "Data Tools",
      "category": "Data & AI",
      "tagline": "Data utilities for agents — HTTP fetching, array processing, transformation, currency conversion, and PDF extraction.",
      "tags": [
        "data",
        "utilities"
      ],
      "useCases": [
        "Fetch and transform JSON from external APIs",
        "Convert and reconcile currency amounts",
        "Parse PDFs and extract their text and metadata",
        "Chunk, deduplicate, and reshape datasets"
      ],
      "permissionsSummary": "Local data and file utilities plus outbound HTTP fetch. No stored credentials and no destructive external actions.",
      "links": {
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-data"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "5700ae0991a825b8bdef6f9c08bc92c4571fa54b",
      "notices_url": "https://orgsdk.ai/plugins/packages/data/1.0.3/caef4b5ce808d7d80e8f428971daaa9474261377cc24b7593a5c43b298d789f1.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Current execution contract\n\n`fetchData` supports deliberate HTTP/HTTPS access, including private/loopback destinations. Run it only with the network authority appropriate to the project; the plugin is not an egress filter. Responses are limited to 2 MB with a 15-second I/O deadline; HTTP errors fail. PDF input is limited to 10 MB, output to 2 MB and 100 pages. `convertCurrency` uses frozen demonstration rates, not live financial data.\n\nNative OAuth providers receive only `{source: \"byo\", secret_accessToken}`. The host owns refresh. The first-party relay is retired.\n\n## Historical source reference\n\nThe following describes the former author-source host; current npm and native credential behaviour above takes precedence.\n\n# Data Tools\n\nData utilities: HTTP fetching, array processing, transformation, currency\nconversion, and PDF text extraction. No credentials required.\n\n## What it does\n\nThe `data` plugin is the Swiss-army knife for shaping data inside workflows. It\ncovers the common pipeline stages — fetch → transform → validate → store — plus\nniche helpers (currency conversion, PDF parsing) that would otherwise each need\ntheir own integration. Everything runs in-process; nothing leaves the sandbox\nexcept the explicit `fetchData` HTTP call.\n\nTypical uses:\n\n- **Fetch + transform** — pull JSON from an API, group it, and dedupe before\n  writing it to a sheet (compose with `google-sheets`).\n- **Invoice parsing** — extract text from a PDF attachment (compose with\n  `gmail` + `llm`) and structure it.\n- **Currency normalisation** — convert amounts to a reporting currency.\n- **Bulk validation** — validate an array of records against rules before\n  persisting.\n\n## Functions\n\n| Function | Purpose |\n|---|---|\n| `fetchData` | Fetch JSON data from a URL. |\n| `convertCurrency` | Convert an amount between currencies. Returns `{ convertedAmount, rate, ... }`. |\n| `parsePdf` | Extract text + metadata from a base64-encoded PDF. |\n| `processData` | Process a data array (selectable processor). |\n| `transformData` | Transform a data array (selectable transformer + key). |\n| `chunkArray` | Split an array into chunks of a given size. |\n| `uniqueBy` | Remove duplicates from an array by key. |\n| `groupBy` | Group array items by key. |\n| `sumArray` | Sum all numbers in an array. |\n| `validateData` | Validate data against a rules object. |\n| `saveResult` | Save a result into a storage object (in-workflow). |\n\nThe full function signatures (parameters, return types) are available via the\n`plugins:info` command in the dashboard.\n\n## Credentials\n\n**None.** This plugin has no auth block and no `requiredCredentials`. Every\nfunction is immediately callable once the plugin is enabled on an agent. Note\nthat `fetchData` makes outbound HTTP — the target URL is your responsibility.\n\n## Using it in a workflow\n\n```typescript\n// Fetch, dedupe, and group\nconst raw = await fetchData(\"https://api.example.com/orders\");\nconst deduped = uniqueBy(raw, \"orderId\");\nconst byRegion = groupBy(deduped, \"region\");\n\n// Convert a single amount\nconst aud = await convertCurrency(100, \"USD\", \"AUD\");\nconsole.log(aud.convertedAmount, aud.rate);\n\n// Extract text from a Gmail PDF attachment (compose with gmail + llm)\n// const att = await getMessageAttachment({ ... });\n// const pdf = await parsePdf(att.dataBase64);\n```\n\n## Notes\n\n- `convertCurrency` uses a fixed rate table, not a live feed — verify the rate\n  freshness for financial use cases.\n- `parsePdf` accepts **standard base64** — pass the `dataBase64` field from\n  `getMessageAttachment()` directly.\n- No `guides/` reference yet; agent usage patterns can be added later.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "convertCurrency",
        "description": "Convert currency amount from one currency to another. IMPORTANT: Returns OBJECT with {convertedAmount: number, originalAmount: number, rate: number}. Use result.convertedAmount to get the numeric converted value. Example: const result = await convertCurrency(100, 'USD', 'AUD'); const converted = result.convertedAmount; Available rates: USD_TO_AUD=1.53, EUR_TO_AUD=1.67, GBP_TO_AUD=1.92",
        "returnType": "Promise<{ originalAmount: number; fromCurrency: string; convertedAmount: number; toCurrency: string; rate: number }>",
        "parameters": [
          {
            "name": "amount",
            "type": "number"
          },
          {
            "name": "fromCurrency",
            "type": "string"
          },
          {
            "name": "toCurrency",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "fetchData",
        "description": "Fetch JSON data from a URL",
        "returnType": "Promise<any>",
        "parameters": [
          {
            "name": "url",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "network",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "sumArray",
        "description": "Sum all numbers in an array",
        "returnType": "number",
        "parameters": [
          {
            "name": "numbers",
            "type": "number[]"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "chunkArray",
            "groupBy"
          ]
        }
      },
      {
        "name": "chunkArray",
        "description": "Split array into chunks of specified size",
        "returnType": "T[][]",
        "parameters": [
          {
            "name": "array",
            "type": "T[]"
          },
          {
            "name": "size",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "sumArray",
            "uniqueBy"
          ]
        }
      },
      {
        "name": "uniqueBy",
        "description": "Remove duplicates from array by key",
        "returnType": "T[]",
        "parameters": [
          {
            "name": "array",
            "type": "T[]"
          },
          {
            "name": "key",
            "type": "keyof T"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "groupBy",
            "chunkArray"
          ]
        }
      },
      {
        "name": "groupBy",
        "description": "Group array items by key",
        "returnType": "Record<string, T[]>",
        "parameters": [
          {
            "name": "array",
            "type": "T[]"
          },
          {
            "name": "key",
            "type": "keyof T"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "uniqueBy",
            "sumArray"
          ]
        }
      },
      {
        "name": "processData",
        "description": "Process data array",
        "returnType": "any[]",
        "parameters": [
          {
            "name": "data",
            "type": "any[]"
          },
          {
            "name": "processor",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "transformData"
          ]
        }
      },
      {
        "name": "transformData",
        "description": "Transform data array with various operations",
        "returnType": "any",
        "parameters": [
          {
            "name": "data",
            "type": "any[]"
          },
          {
            "name": "transformer",
            "type": "string"
          },
          {
            "name": "key",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "processData"
          ]
        }
      },
      {
        "name": "saveResult",
        "description": "Save a result to storage",
        "returnType": "{ key: string; value: any; saved: boolean }",
        "parameters": [
          {
            "name": "key",
            "type": "string"
          },
          {
            "name": "value",
            "type": "any"
          },
          {
            "name": "storage",
            "type": "Record<string, any>"
          }
        ]
      },
      {
        "name": "validateData",
        "description": "Validate data against rules",
        "returnType": "{ valid: boolean; errors: string[] }",
        "parameters": [
          {
            "name": "data",
            "type": "any"
          },
          {
            "name": "rules",
            "type": "Record<string, any>"
          }
        ]
      },
      {
        "name": "parsePdf",
        "description": "Extract text content and metadata from a PDF. Accepts standard base64 string (e.g. from getMessageAttachment().dataBase64). Returns full text, page count, and optional metadata.",
        "returnType": "Promise<{ text: string; totalPages: number; metadata?: { title?: string; author?: string; subject?: string; creator?: string; producer?: string } }>",
        "parameters": [
          {
            "name": "base64Data",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessageAttachment"
          ]
        },
        "docs": {
          "summary": "Extract text content and metadata from a PDF document. Accepts a standard base64-encoded string — the same format returned by getMessageAttachment().dataBase64.",
          "parameters": [
            {
              "name": "base64Data",
              "type": "string",
              "description": "Standard base64-encoded PDF bytes. Pass the dataBase64 field from getMessageAttachment() directly.",
              "example": "\"JVBERi0xLjQK...\""
            }
          ],
          "returns": {
            "type": "Promise<{ text: string; totalPages: number; metadata?: Record<string, string> }>",
            "description": "Extracted text (all pages merged), total page count, and optional PDF metadata (title, author, subject, creator, producer).",
            "example": "{ text: \"Invoice #1234\\nDate: 2026-01-15\\nAmount: $500.00\", totalPages: 2, metadata: { title: \"Invoice\", author: \"Acme Corp\" } }"
          },
          "examples": [
            {
              "title": "Extract text from a Gmail PDF attachment",
              "code": "const msgs = await gmail.getMessages(\"has:attachment\", 5);\nconst msg = msgs[0];\nconst pdfRef = msg.attachments?.find(a => a.filename.endsWith(\".pdf\"));\nif (pdfRef) {\n  const attachment = await gmail.getMessageAttachment(msg.id, pdfRef.attachmentId);\n  const pdf = await data.parsePdf(attachment.dataBase64);\n  pdf.text;\n}",
              "description": "Full chain: find email with attachment, download it, extract text"
            }
          ],
          "caveats": [
            "Scanned/image-only PDFs will return empty or minimal text — OCR is not supported",
            "Very large PDFs may be slow; consider processing only the attachments you need"
          ],
          "seeAlso": [
            "getMessageAttachment",
            "getMessage"
          ]
        }
      }
    ]
  },
  {
    "slug": "browser",
    "version": "1.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/browser/1.1.2/fc04d57a18ade760c5af3c57161f2af027a751822a2f2282b3cee0a7f1c12ee9.mjs",
    "digest": "fc04d57a18ade760c5af3c57161f2af027a751822a2f2282b3cee0a7f1c12ee9",
    "contract_version": 1,
    "display_name": "Browser",
    "description": "Browser automation: hardened PDF generation from HTML via headless Chromium",
    "category": "Documents & Output",
    "auth": {},
    "marketplace": {
      "displayName": "Browser",
      "category": "Documents & Output",
      "tagline": "Hardened headless browser tooling for agents — render HTML to PDF with JavaScript disabled and all external requests blocked.",
      "tags": [
        "automation",
        "pdf",
        "documents"
      ],
      "useCases": [
        "Generate a PDF report from agent output and email it",
        "Render invoices, receipts, or summaries to PDF",
        "Produce a downloadable artifact from a workflow"
      ],
      "permissionsSummary": "Local headless browser rendering only. JavaScript is disabled and all external/resource requests are blocked. No credentials, no network egress, no destructive actions.",
      "links": {
        "sourceRepo": "https://github.com/OrgSDK/orgsdk-plugin-browser"
      },
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "e375ad2dbe973268064d53caa68884f379afd287",
      "notices_url": "https://orgsdk.ai/plugins/packages/browser/1.1.2/fc04d57a18ade760c5af3c57161f2af027a751822a2f2282b3cee0a7f1c12ee9.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Browser\n\nRender HTML to PDF using headless Chromium. No credentials required — every\nfunction is callable out of the box.\n\n## What it does\n\nThe `browser` plugin gives agent workflows the ability to produce\nprint-ready documents: invoices, reports, receipts, and any structured output\nthat benefits from real CSS layout. It spins up headless Chromium, renders the\nsupplied HTML, and returns base64-encoded PDF bytes.\n\nTypical uses:\n\n- **Invoice generation** — render a styled invoice from extracted data and\n  attach it to an email (compose with the `gmail` plugin).\n- **Report export** — turn a workflow's structured output into a PDF summary.\n- **Receipts** — generate branded receipts after a completed action.\n\n## Functions\n\n| Function | Purpose |\n|---|---|\n| `generatePdf` | Render full HTML (with CSS, tables, images as data URIs) to base64-encoded PDF bytes. |\n\nThe full function signature (parameters, return type) is available via the\n`plugins:info` command in the dashboard.\n\n## Credentials\n\n**None.** This plugin has no auth block and no `requiredCredentials` — it runs\nentirely within the sandbox. Every function is immediately callable once the\nplugin is enabled on an agent.\n\n## Using it in a workflow\n\n```typescript\nconst pdf = await generatePdf({\n  html: `\n    <html>\n      <body style=\"font-family: sans-serif;\">\n        <h1>Invoice #1042</h1>\n        <table>\n          <tr><td>Consulting</td><td>$1,200.00</td></tr>\n          <tr><td><strong>Total</strong></td><td><strong>$1,200.00</strong></td></tr>\n        </table>\n      </body>\n    </html>\n  `,\n});\n\n// `pdf` is base64 bytes — attach to an email (gmail plugin) or store as an\n// artifact. Images must be embedded as data URIs; external URLs are not fetched.\n```\n\n## Security model (v1.1.0)\n\nThe renderer is hardened against agent-authored HTML:\n\n- **JavaScript is disabled.** A Content-Security-Policy\n  (`script-src 'none'`) is injected into the document head, so `<script>`\n  tags and inline event handlers never execute.\n- **All external/resource requests are blocked.** Every subresource request is\n  intercepted and aborted unless it uses a safe scheme (`data:`, `blob:`,\n  `about:`). This blocks `http`/`https` (external and private-network), `file`,\n  `ftp`, and `ws` — no exfiltration, no SSRF, no local-file reads.\n- **Renders are serialized** — at most one Chromium render runs per daemon\n  process at a time.\n- **Hard ceilings:** 10 MB HTML input, 50 MB PDF output, and a 30 s render\n  timeout (launch + render + pdf).\n- **Guaranteed cleanup:** the page and browser are closed in `finally`.\n- **Sanitized errors:** thrown messages never include HTML content, file paths,\n  or secrets.\n\nThe Chromium browser binary is provided by the daemon image\n(`PLAYWRIGHT_BROWSERS_PATH`); the Playwright JS package is self-contained in\nthe plugin's own `node_modules` and is **not** shared across the daemon's\nruntime pool.\n\n## Notes\n\n- Images and fonts must be **embedded as data URIs** — external resources are\n  blocked (see Security model).\n- The returned bytes are base64-encoded; decode before writing to a file or\n  pass directly to an email attachment function.\n- No `guides/` reference yet; agent usage patterns can be added later.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "generatePdf",
        "description": "Generate a PDF from HTML content using headless Chromium. Returns base64-encoded PDF bytes.",
        "returnType": "Promise<{ base64: string; sizeBytes: number; filename: string }>",
        "parameters": [
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "options",
            "type": "{ format?: \"A4\" | \"Letter\"; landscape?: boolean; margin?: { top?: string; right?: string; bottom?: string; left?: string }; filename?: string }"
          }
        ],
        "metadata": {
          "category": "browser",
          "tags": [
            "io"
          ],
          "version": "1.1.2",
          "relatedTo": [
            "parsePdf",
            "sendMessageWithAttachment",
            "upsertArtifact"
          ]
        },
        "docs": {
          "summary": "Generate a PDF document from HTML content. Uses headless Chromium via Playwright to render the HTML and produce a PDF. Returns base64-encoded bytes suitable for storing as an artifact or attaching to an email. JavaScript is disabled and all external/resource requests are blocked — only inline styles and data:/blob: images load.",
          "parameters": [
            {
              "name": "html",
              "type": "string",
              "description": "Full HTML content to render as a PDF. Inline CSS and tables are supported; images must be embedded as data URIs.",
              "example": "\"<html><body><h1>Invoice #1001</h1><table>...</table></body></html>\""
            },
            {
              "name": "options",
              "type": "object",
              "description": "Optional PDF generation settings.",
              "example": "{ format: \"A4\", landscape: false, filename: \"invoice-1001.pdf\" }"
            }
          ],
          "returns": {
            "type": "Promise<{ base64: string; sizeBytes: number; filename: string }>",
            "description": "Base64-encoded PDF bytes, file size in bytes, and the filename (auto-generated if not provided).",
            "example": "{ base64: \"JVBERi0xLjQK...\", sizeBytes: 45230, filename: \"invoice-1001.pdf\" }"
          },
          "examples": [
            {
              "title": "Generate an invoice PDF and email it",
              "code": "const html = `<html>\n<head><style>table { width: 100%; border-collapse: collapse; } td, th { border: 1px solid #ddd; padding: 8px; }</style></head>\n<body>\n  <h1>Invoice #1001</h1>\n  <table><tr><th>Item</th><th>Amount</th></tr><tr><td>Consulting</td><td>$500</td></tr></table>\n</body></html>`;\n\nconst pdf = await generatePdf({ html, options: { format: \"A4\", filename: \"invoice-1001.pdf\" } });\nawait upsertArtifact(\"report\", { base64: pdf.base64, filename: pdf.filename }, { name: \"invoice-1001\", tags: [\"invoice\"] });\nawait sendMessageWithAttachment(\"client@example.com\", \"Invoice #1001\", \"Please find your invoice attached.\", [{ filename: pdf.filename, mimeType: \"application/pdf\", base64Data: pdf.base64 }]);",
              "description": "Generate an invoice PDF from HTML, store it as an artifact, and email it"
            }
          ],
          "caveats": [
            "JavaScript is disabled — <script> tags and inline event handlers do not execute.",
            "All external resource requests (http, https, file, fonts, remote images) are blocked. Embed images and fonts as data URIs.",
            "Renders are serialized (one at a time per process) and capped at a 30s render timeout."
          ],
          "seeAlso": [
            "parsePdf",
            "sendMessageWithAttachment",
            "upsertArtifact"
          ]
        }
      }
    ]
  },
  {
    "slug": "asana",
    "version": "0.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/asana/0.1.2/60577d2852389aca9878689a5042c5208da96a41afa7b9509898d1fa47ac1a81.mjs",
    "digest": "60577d2852389aca9878689a5042c5208da96a41afa7b9509898d1fa47ac1a81",
    "contract_version": 1,
    "display_name": "Asana",
    "description": "Read-only bounded asana workflow evidence for OrgSDK.",
    "category": "Productivity",
    "auth": {
      "type": "token",
      "provider": "asana",
      "label": "asana credential",
      "hint": "Injected secrets only; see README for setup and permission boundaries."
    },
    "marketplace": {
      "displayName": "Asana",
      "category": "Productivity",
      "permissionsSummary": "Read-only evidence retrieval, constrained by mandatory configured resource boundaries. No external writes.",
      "license": "Apache-2.0",
      "license_url": "https://orgsdk.ai/plugins/licenses/apache-2.0.txt",
      "source_commit": "20db0d5de8a80479643c0f34ad7c6977fa33f82c",
      "notices_url": "https://orgsdk.ai/plugins/packages/asana/0.1.2/60577d2852389aca9878689a5042c5208da96a41afa7b9509898d1fa47ac1a81.notices.txt"
    },
    "documentation": "# Install from the OrgSDK catalogue\n\nEnable this pinned release in your project Plugins page. OrgSDK downloads its versioned HTTPS bundle and verifies the SHA256 digest. npm installation is not required.\n\n# Asana evidence plugin\n\n## Current npm distribution and execution checks\n\nUpdated **31 October 2026**. This public-release candidate uses `@orgsdk/plugins@0.2.2`\nand `@orgsdk/executor@0.1.0` (with the tracked packaging compatibility patch).\n`bun run build:npm` creates a public Apache-2.0 npm payload; `bun run test:executor`\ndownloads the extracted npm entry into an empty executor store and executes every exported function headlessly.\n`bun run test:registration` verifies fresh-project registration using the npm host.\nSee [npm distribution, CI activation and verification](docs/npm-distribution.md)\nfor prerequisites, commands, fixtures and release settings. Publication remains\nan explicit release operation; source-root `private: true` is retained.\n\nUse project notes and task evidence to compare actual requests with an agreed agency scope. The plugin supplies observations for a review; it does not infer approvals or update tasks.\n\n## Install in OrgSDK\n\nOpen your project's Plugins page, select this plugin and enable the pinned release.\nThe default catalogue is https://orgsdk.ai/plugins/releases.json. The executor\ndownloads the versioned bundle and verifies its SHA256. Configure the provider\nand attach the credential described below before running an action.\n\nThe plugin is Apache-2.0 licensed; see LICENSE, NOTICE and TRADEMARKS.md.\nThe generated npm distribution is a separate publication surface. Installing npm\nfiles alone does not register a project plugin. Local builds do not publish it.\n\n## Setup\n\nCreate an Asana personal access token for the intended operator through the Asana developer console, then attach an OrgSDK `asana` credential with `secret_apiKey`. PATs act with the user's permissions and can grant more authority than this read-only plugin uses. This implementation is for an operator's private integration; Asana recommends OAuth for apps acting on behalf of multiple users. No OAuth flow is implemented here.\n\nConfigure `{ \"workspaceId\": \"123\", \"projectIds\": \"456,789\" }`. Both fields are mandatory. The project allowlist accepts 1-20 numeric IDs. Every project read verifies its workspace; every direct task read verifies workspace and membership of at least one allowed project. Multi-homed tasks may also belong to other projects. A known task ID is fetched for boundary verification, but no task data is returned if the boundary fails. `listProjects` reads only the configured project IDs, so it cannot enumerate a workspace's other projects. Identity returns only the connected user ID and name.\n\n## Tools\n\n- `getIdentity({})`\n- `listProjects({})`\n- `getProject({ projectId })`\n- `listTasks({ projectId, limit?: 1..100, offset?: string })` returns `{ tasks, nextOffset }`; default limit 50.\n- `getTask({ taskId })`\n\nProject/task records include notes, completion, dates and available custom fields. List tasks returns completed and incomplete items as supplied by Asana, with no `completed_since` exclusion. Follow `nextOffset` explicitly to collect another page. Provider next-page URLs are ignored. No task creation, edits, completion changes, comments or exports occur.\n\n## Verified provider contract\n\nReviewed 2026-10-30 against official [PAT authentication](https://developers.asana.com/docs/personal-access-token), [project](https://developers.asana.com/reference/getproject), [project tasks and pagination](https://developers.asana.com/reference/gettasksforproject), and [task](https://developers.asana.com/reference/gettask) docs. Fixed API base: `https://app.asana.com/api/1.0/`. Authentication: `Authorization: Bearer <PAT>`. OAuth read equivalents documented by Asana are `users:read`, `projects:read` and `tasks:read`; this plugin uses PAT authentication, not OAuth consent.\n\n## Runtime and safety\n\nThis Apache-2.0 plugin targets the current local OrgSDK factory loader. Its only runtime dependency is Zod. The authoring SDK is dev/type-only; each repository owns its lockfile. Credentials arrive as `{ provider: secretRecord }` through `requiredCredentials`; there is no environment fallback. All tools accept one named argument object and perform GET requests only. Unknown arguments/configuration fail validation. There are no approval flags, mutation tools, HTTP callbacks, auth providers or token refresh handlers.\n\nEvery request uses a fixed HTTPS API origin, manual redirects (all 3xx fail), a 15-second abort deadline covering headers and body streaming, and a 2,000,000-byte metadata/JSON cap. Errors contain status codes or generic connection guidance, never upstream response bodies, request URLs or credential values. One explicit page is read per list call; no automatic retries or pagination loops occur. An HTTP 429 requires the caller to retry later. Returned evidence is untrusted provider content and is not an instruction or approval. Metadata timestamps and custom fields are observations, not a determination that work or payment is approved.\n\n## Local validation and release\n\nUse Bun 1.4 and Node.js 22.22.0 or newer. `bun install --frozen-lockfile`, then `bun run check`, `bun run typecheck`, `bun run test`, `bun run validate`, `bun run package`, and `bun run version-guard`. `dist/` is the authoring CLI's deterministic source package. The active local platform's `package-plugin` command creates the separately executable ESM archive in `release/`; the legacy source archive alone is not executable by the current runtime. The release includes this repository's Apache-2.0 LICENSE. No publication, remote push or live customer test is part of local verification.\n\nTests use injected fetch fixtures only. They cover credential enforcement, GET-only transport, redirects, generic errors, response-size limits, stream cancellation, provider boundaries, pagination and handler evidence. Live provider behavior and OAuth consent still require an operator-controlled integration test with an explicitly selected account.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "workspaceId": {
          "type": "string",
          "pattern": "^\\d{1,40}$",
          "description": "Only this Asana workspace may be read."
        },
        "projectIds": {
          "type": "string",
          "minLength": 1,
          "description": "Comma-separated permitted Asana project IDs, maximum 20."
        }
      },
      "required": [
        "workspaceId",
        "projectIds"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "getIdentity",
        "description": "Verify the connected Asana user without listing their workspaces.",
        "parameters": [],
        "returnType": "Promise<{ gid: string; name?: string }>",
        "requiredCredentials": [
          "asana"
        ],
        "metadata": {
          "readonly": true,
          "category": "asana",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "listProjects",
        "description": "Read only configured projects for approved-scope comparisons.",
        "parameters": [],
        "returnType": "Promise<Record<string, unknown>[]>",
        "requiredCredentials": [
          "asana"
        ],
        "metadata": {
          "readonly": true,
          "category": "asana",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "getProject",
        "description": "Read an allowed project's notes, dates and custom fields.",
        "parameters": [
          {
            "name": "projectId",
            "type": "string"
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "asana"
        ],
        "metadata": {
          "readonly": true,
          "category": "asana",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "listTasks",
        "description": "Read one page of tasks with notes, completion, dates and custom fields.",
        "parameters": [
          {
            "name": "projectId",
            "type": "string"
          },
          {
            "name": "limit",
            "type": "number",
            "optional": true
          },
          {
            "name": "offset",
            "type": "string",
            "optional": true
          }
        ],
        "returnType": "Promise<{ tasks: Record<string, unknown>[]; nextOffset: string | null }>",
        "requiredCredentials": [
          "asana"
        ],
        "metadata": {
          "readonly": true,
          "category": "asana",
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "getTask",
        "description": "Read a task only when it belongs to the configured workspace and an allowed project.",
        "parameters": [
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "returnType": "Promise<Record<string, unknown>>",
        "requiredCredentials": [
          "asana"
        ],
        "metadata": {
          "readonly": true,
          "category": "asana",
          "tags": [
            "readonly"
          ]
        }
      }
    ]
  },
  {
    "slug": "browser-cdp",
    "version": "0.1.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/browser-cdp/0.1.2/3bd0890a2fa5a498c19ba90a8dc2a0d51fdb90f870985de5dfe2e945e13d2d17.mjs",
    "digest": "3bd0890a2fa5a498c19ba90a8dc2a0d51fdb90f870985de5dfe2e945e13d2d17",
    "contract_version": 1,
    "display_name": "Browser CDP",
    "description": "Control a remote Chrome browser over CDP with Playwright. Connect to self-hosted Steel, Steel Cloud, or another Chromium CDP endpoint.",
    "category": "Developer",
    "auth": {
      "type": "token",
      "provider": "browser-cdp",
      "label": "Browser connection credentials (optional)",
      "hint": "For Steel Cloud or bearer authentication, attach a credential with secret_apiKey. Self-hosted Steel can use authMode none without a credential. Optional secret_headers contains JSON connection headers."
    },
    "marketplace": {
      "displayName": "Browser CDP",
      "category": "Developer",
      "tagline": "Use a remote Chrome browser from reusable OrgSDK actions.",
      "tags": [
        "browser",
        "automation",
        "playwright",
        "cdp",
        "steel"
      ],
      "useCases": [
        "Automate websites with an existing Steel session",
        "Extract page content and capture screenshots",
        "Continue browser workflows across project runs"
      ],
      "permissionsSummary": "Controls pages in the connected browser's default context, including navigation, form submission, page JavaScript, screenshots and closing selected tabs. Uses that session's cookies and network access. Does not launch or terminate the browser.",
      "links": {
        "homepage": "https://orgsdk.ai",
        "docs": "https://github.com/steel-dev/steel-browser"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/browser-cdp/0.1.2/3bd0890a2fa5a498c19ba90a8dc2a0d51fdb90f870985de5dfe2e945e13d2d17.notices.txt",
      "license_url": "/plugins/browser-cdp/license.txt"
    },
    "documentation": "# OrgSDK Browser CDP\n\nA project-owned external OrgSDK plugin for controlling an existing Chromium\nbrowser through Playwright's Chrome DevTools Protocol connection. Works with\nself-hosted [Steel Browser](https://github.com/steel-dev/steel-browser), Steel\nCloud, and Chrome's remote debugging endpoint. Slug: `browser-cdp`; OrgSDK\nnamespace: `browserCdp`.\n\nThe executor only needs this plugin's JavaScript. `playwright-core` is embedded\nin the release; no Chromium installation or Steel SDK is needed on the executor.\n\n## Install in a project\n\nThe npm distribution is `@orgsdk/plugin-browser-cdp`. Once a version is\npublished, install that exact version and use its `plugin.catalog.json` as an\nOrgSDK release catalogue. Installing npm files alone does not register the\nplugin: add the pinned external release in your project's Plugins page, then\nconfigure the endpoint and optional credential. The source build emits this\ncatalogue at `dist/npm/plugin.catalog.json`. Its versioned npm CDN URL becomes\navailable only after publication; the executor verifies the bundle's SHA256.\nBuilding or testing locally does not publish anything.\n\n## Configure\n\nSelf-hosted Steel (its **CDP WebSocket is on the API port 3000**, not the optional\nconsole debugger port 9223):\n\n```json\n{\n\t\"cdpEndpoint\": \"ws://steel:3000\",\n\t\"authMode\": \"none\"\n}\n```\n\nUse the hostname reachable **from the executor**. `localhost` inside a Docker\njob refers to that container. Steel must already be running and its browser\nsession must remain alive between calls.\n\nFor Chrome configured with remote debugging, use `http://chrome:9222` or its\nfull `ws://.../devtools/browser/...` URL. Both HTTP(S) discovery and WS(S)\nconnections are supported.\n\nFor an existing Steel Cloud session:\n\n```json\n{\n\t\"cdpEndpoint\": \"wss://connect.steel.dev\",\n\t\"authMode\": \"steel\",\n\t\"sessionId\": \"YOUR_EXISTING_SESSION_ID\",\n\t\"connectTimeoutMs\": 15000,\n\t\"actionTimeoutMs\": 10000\n}\n```\n\nAttach an OrgSDK credential with provider `browser-cdp` and secret data:\n\n```json\n{ \"secret_apiKey\": \"YOUR_STEEL_API_KEY\" }\n```\n\n`authMode: \"steel\"` adds the key as the `apiKey` URL parameter, and `sessionId`\nadds or replaces the session parameter. An existing session ID is required in Steel mode to prevent a fresh cloud\nsession on every call. Session creation, renewal and release are managed\nthrough Steel itself. This plugin does not invoke Steel's REST API.\n\nFor an authenticated CDP proxy, `authMode: \"bearer\"` sends `secret_apiKey` as\n`Authorization: Bearer <secret_apiKey>`. Additional connection headers can be supplied as\nJSON in the encrypted credential's `secret_headers` string:\n\n```json\n{ \"secret_headers\": \"{\\\"X-Browser-Token\\\":\\\"YOUR_TOKEN\\\"}\" }\n```\n\nHeaders also work with `authMode: \"none\"`. Keep keys and auth headers in the\ncredential, not the plain configuration URL. No credential is needed for an\nunauthenticated self-hosted endpoint. Credentials are injected by OrgSDK at call\ntime; the plugin does not read environment tokens or write credential stores.\n\n## Use from OrgSDK\n\n```typescript\nconst tab = await browserCdp.openPage({ url: \"https://example.com\" });\nconsole.log(await browserCdp.snapshot({ pageId: tab.pageId }));\nconsole.log(await browserCdp.readText({ pageId: tab.pageId }));\nconst image = await browserCdp.screenshot({ pageId: tab.pageId });\n// image = { mimeType: \"image/png\", base64: \"...\" }\nawait browserCdp.closePage({ pageId: tab.pageId });\n```\n\n```typescript\nconst tabs = await browserCdp.listPages({});\nconst pageId = tabs[0].pageId;\nawait browserCdp.fill({\n\tpageId,\n\tselector: \"#email\",\n\tvalue: \"test@example.com\",\n});\nawait browserCdp.click({ pageId, selector: 'role=button[name=\"Continue\"]' });\nconsole.log(await browserCdp.readText({ pageId, selector: \"main\" }));\n```\n\n| Tool           | Purpose                                                       |\n| -------------- | ------------------------------------------------------------- |\n| `listPages`    | List default-context tabs with stable `pageId`, URL and title |\n| `openPage`     | Create a tab, optionally navigate to an HTTP(S) URL           |\n| `navigate`     | Navigate a tab and return URL, title and HTTP status          |\n| `snapshot`     | Read an accessibility tree, optionally scoped to a selector   |\n| `readText`     | Read visible text, optionally scoped to a selector            |\n| `click`        | Click a matching element                                      |\n| `fill`         | Replace an input or textarea value                            |\n| `press`        | Send a key or shortcut to a matching element                  |\n| `selectOption` | Select one or more values in a select element                 |\n| `screenshot`   | Return a PNG as base64; `fullPage` defaults to false          |\n| `evaluate`     | Evaluate page JavaScript and return a serializable result     |\n| `closePage`    | Close the explicitly selected tab                             |\n\nTools accept named argument objects. Locators use Playwright selector syntax;\naccessibility snapshots describe controls but do not assign reusable element\nreferences. Text/snapshot output defaults to 20,000 characters, with a\n`truncated` flag and optional `maxChars` up to 200,000. Navigation defaults to\n`domcontentloaded`; `waitUntil` also supports `commit` and `load`.\n\nEach call makes a fresh connection, uses the existing default context (including\nits cookies), and disconnects in `finally`. It never creates/closes a context,\nlaunches a browser or releases a Steel session. Stable CDP target IDs survive\nplugin/executor process restarts while the remote tab lives. An expired session\nor closed tab requires `listPages` or `openPage` again. If initial navigation\nfails, `openPage` closes the tab it just created. Other failures leave existing\ntabs in place. There is no automatic retry of mutating operations.\n\nConnection timeout defaults to 15 seconds and the total operation budget to 10\nseconds; both are configurable up to 120 seconds. Timed-out operations disconnect\nbut cannot undo a click, form submission or page JavaScript already sent to the\nbrowser. An evaluation that never settles is bounded by the operation budget.\nConnection errors omit remote URLs and headers to avoid disclosing credentials.\n\nThis is a trusted browser automation plugin: it can use the remote session's\ncookies and network access, submit forms, execute page JavaScript, and close\ntabs. Assign a dedicated browser/session to each OrgSDK project. Sharing one\nendpoint shares its default-context tabs and cookies, and concurrent actions on\nthe same tab may conflict. CDP supports Chromium only and has lower fidelity\nthan Playwright's native protocol; browser/server compatibility still matters.\n\n## Develop and verify\n\nRequires Bun 1.4, Node.js 22.22.0+, GNU tar for source packaging, and independently installed\ndependencies. The package command normalizes archive timestamps, ownership and\npermissions so repeated source builds have the same checksum:\n\n```sh\nbun install --frozen-lockfile\nbun run check\nbun run typecheck\nbun run test\nbun run validate\nbun run package\nbun run version-guard\n```\n\nThe dependency lock pins Playwright and playwright-core to 1.64.1. Source\nintegration and extracted npm tests need the matching headless Chromium in an\nisolated cache; deployed executors only need access to an existing remote browser.\nUse `bunx --no-install playwright install chromium` with an explicit\n`PLAYWRIGHT_BROWSERS_PATH` when provisioning a test cache. Source integration also\nrequires `playwright-cli` on PATH and saves `.artifacts/integration.png`.\n\nRun `bun run package` before `bun run build:npm`, because source packaging\nrecreates `dist/`. Then run `bun run test:executor` and\n`bun run test:registration` with the matching browser-cache environment. These\ntests use disposable profiles, loopback fixture sites and fresh OrgSDK stores,\nnot a personal browser or installation. They verify every named operation from\nthe extracted npm payload and fresh-project registration; they do not prove\nlive Steel Cloud credentials, provider uptime or a public release URL.\n\n## Build an executable OrgSDK release\n\nThe authoring SDK's `bun run package` produces a source artifact. The local\nOrgSDK executor requires its self-contained ESM adapter instead:\n\n```sh\norgsdk-executor package-plugin \\\n  --directory \"$PWD\" --output \"$PWD/release\" \\\n  --embed-dependencies playwright-core --license \"$PWD/LICENSE\"\n```\n\nOr use the platform checkout's `runtime/src/cli.ts` with Bun. Embedding\n`playwright-core` preserves its package-relative assets. The release contains\n`browser-cdp.mjs`, digest-pinned manifest and catalog JSON, and third-party\nnotices. Its `package_url` is blank for local installation.\n\nPreinstall into the selected project's executor:\n\n```sh\norgsdk-executor install --agent PROJECT_ID \\\n  --manifest release/browser-cdp.manifest.json \\\n  --file release/browser-cdp.mjs --data /path/to/executor-data\n```\n\nFor project installation, the current platform requires a non-empty HTTPS\n`package_url` even when the bundle is preinstalled (its create/update handlers\ncurrently reject the blank local-only URL). Host the versioned bundle at an\noperator-controlled HTTPS URL, set that URL in the catalog entry, and add the\nentry to the operator's `PLUGIN_CATALOG_PATH`. Then install it in the same\nOrgSDK project and supply the connection configuration above. The emitted\nSHA256 must remain unchanged; a source tarball is not an executable release.\nWhen combining with an existing local catalog, retain its other entries. A local\npreinstall alone does not grant the project permission to use the plugin.\n\nTo test an executable bundle instead of source, set\n`BROWSER_PLUGIN_ENTRY=/absolute/path/to/release/browser-cdp.mjs` when running\n`bun run test:integration`. Also set `ORGSDK_RUNTIME_DIR=/path/to/OrgSDK/runtime`\nto install the bundle into temporary executor storage and exercise every tool\nthrough the real OrgSDK loader and `browserCdp` sandbox namespace.\n\nSource packaging and npm distribution are distinct artifacts. The prepared\n`publish-npm.yml` workflow is gated: publication needs explicit approval, the\nrepository's release variable, the protected `npm-publish` environment and npm\nauthentication. It publishes only its verified tarball with public access and\nprovenance. None of these build/test commands publishes or deploys anything.\n\nThis plugin is Apache-2.0 licensed; see `LICENSE`, `NOTICE` and `TRADEMARKS.md`.\nPlaywright is Apache-2.0 and Zod is MIT. Their dependency notices accompany the\nexecutable bundle and npm package. This grant does not relicense the remaining\nOrgSDK platform or executor.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "cdpEndpoint": {
          "type": "string",
          "format": "uri",
          "description": "Chrome CDP HTTP or WebSocket endpoint. Self-hosted Steel: ws://steel:3000; Chrome: http://chrome:9222; Steel Cloud: wss://connect.steel.dev."
        },
        "authMode": {
          "description": "none for an unauthenticated endpoint; bearer sends the credential token in Authorization; steel adds apiKey to the connection URL.",
          "default": "none",
          "type": "string",
          "enum": [
            "none",
            "bearer",
            "steel"
          ]
        },
        "sessionId": {
          "description": "Existing Steel session ID. Appended as sessionId to the CDP endpoint; session creation and release remain with Steel.",
          "type": "string",
          "minLength": 1
        },
        "connectTimeoutMs": {
          "default": 15000,
          "type": "integer",
          "minimum": 1,
          "maximum": 120000
        },
        "actionTimeoutMs": {
          "default": 10000,
          "type": "integer",
          "minimum": 1,
          "maximum": 120000
        }
      },
      "required": [
        "cdpEndpoint"
      ],
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "listPages",
        "description": "List tabs in the remote browser's default context.",
        "parameters": [],
        "returnType": "Promise<{ pageId: string; url: string; title: string }[]>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": true,
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "openPage",
        "description": "Open a tab in the existing browser context, preserving its cookies.",
        "parameters": [
          {
            "name": "url",
            "type": "string",
            "optional": true
          },
          {
            "name": "waitUntil",
            "type": "\"commit\" | \"domcontentloaded\" | \"load\"",
            "optional": true
          }
        ],
        "returnType": "Promise<{ pageId: string; url: string; title: string }>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      },
      {
        "name": "navigate",
        "description": "Navigate an existing tab to an HTTP(S) URL.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "url",
            "type": "string"
          },
          {
            "name": "waitUntil",
            "type": "\"commit\" | \"domcontentloaded\" | \"load\"",
            "optional": true
          }
        ],
        "returnType": "Promise<{ url: string; title: string; status: number | null }>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      },
      {
        "name": "snapshot",
        "description": "Read the accessibility tree to discover controls. Use selectors for subsequent interactions.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "selector",
            "type": "string",
            "description": "Playwright selector, for example #email, text=Continue or role=button[name=Submit].",
            "optional": true
          },
          {
            "name": "maxChars",
            "type": "number",
            "optional": true
          }
        ],
        "returnType": "Promise<{ snapshot: string; truncated: boolean }>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": true,
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "readText",
        "description": "Read rendered text from a page or matching element.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "selector",
            "type": "string",
            "description": "Playwright selector, for example #email, text=Continue or role=button[name=Submit].",
            "optional": true
          },
          {
            "name": "maxChars",
            "type": "number",
            "optional": true
          }
        ],
        "returnType": "Promise<{ text: string; truncated: boolean }>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": true,
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "click",
        "description": "Click a matching element. May submit forms or cause external changes.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "selector",
            "type": "string",
            "description": "Playwright selector, for example #email, text=Continue or role=button[name=Submit]."
          }
        ],
        "returnType": "Promise<void>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      },
      {
        "name": "fill",
        "description": "Replace the value of a matching input or textarea.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "selector",
            "type": "string",
            "description": "Playwright selector, for example #email, text=Continue or role=button[name=Submit]."
          },
          {
            "name": "value",
            "type": "string"
          }
        ],
        "returnType": "Promise<void>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      },
      {
        "name": "press",
        "description": "Press a key or shortcut on a matching element (for example Enter or Control+A).",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "selector",
            "type": "string",
            "description": "Playwright selector, for example #email, text=Continue or role=button[name=Submit]."
          },
          {
            "name": "key",
            "type": "string"
          }
        ],
        "returnType": "Promise<void>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      },
      {
        "name": "selectOption",
        "description": "Select values in a matching select element.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "selector",
            "type": "string",
            "description": "Playwright selector, for example #email, text=Continue or role=button[name=Submit]."
          },
          {
            "name": "values",
            "type": "string[]"
          }
        ],
        "returnType": "Promise<string[]>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      },
      {
        "name": "screenshot",
        "description": "Capture a PNG screenshot as base64; no remote filesystem path is required.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "fullPage",
            "type": "boolean",
            "optional": true
          }
        ],
        "returnType": "Promise<{ mimeType: \"image/png\"; base64: string }>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": true,
          "tags": [
            "readonly"
          ]
        }
      },
      {
        "name": "evaluate",
        "description": "Evaluate a JavaScript expression in the page and return a serializable value. May change the page or call its APIs.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          },
          {
            "name": "expression",
            "type": "string"
          }
        ],
        "returnType": "Promise<unknown>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      },
      {
        "name": "closePage",
        "description": "Close only the explicitly selected tab. The browser session stays running.",
        "parameters": [
          {
            "name": "pageId",
            "type": "string",
            "description": "Stable CDP page ID returned by listPages or openPage."
          }
        ],
        "returnType": "Promise<void>",
        "requiredCredentials": [
          "browser-cdp"
        ],
        "metadata": {
          "category": "browser",
          "readonly": false,
          "tags": [
            "io"
          ]
        }
      }
    ]
  },
  {
    "slug": "twitter",
    "version": "1.0.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/twitter/1.0.2/b9f3a565e2af619445586ed8b07b96767a434ab9710296dc5150279f94475c71.mjs",
    "digest": "b9f3a565e2af619445586ed8b07b96767a434ab9710296dc5150279f94475c71",
    "contract_version": 1,
    "display_name": "X",
    "description": "Publish posts and threads on X and inspect engagement metrics.",
    "category": "Social",
    "auth": {
      "type": "oauth2",
      "provider": "twitter-oauth",
      "scopes": [
        "tweet.read",
        "tweet.write",
        "users.read",
        "offline.access"
      ],
      "label": "X OAuth",
      "hint": "Use your own provider OAuth application. Connect its client ID and secret in OrgSDK, then select that credential for this project."
    },
    "marketplace": {
      "displayName": "X",
      "category": "Social",
      "tags": [
        "social",
        "posting",
        "analytics"
      ],
      "useCases": [
        "Draft and post tweets on a schedule",
        "Post a thread from long-form content",
        "Track engagement metrics across posts",
        "Reply or quote-tweet from a queue"
      ],
      "permissionsSummary": "Post, delete, and read analytics for tweets via X OAuth. Posting and deleting are public, mutating actions; deletes are irreversible.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "cc140a38f446f954c1600f06b241f410540b0ebc",
      "icon_url": "https://orgsdk.ai/plugins/icons/twitter.svg",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/twitter"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/twitter/1.0.2/b9f3a565e2af619445586ed8b07b96767a434ab9710296dc5150279f94475c71.notices.txt"
    },
    "documentation": "# X plugin\n\nPublish posts and threads on X and inspect engagement metrics.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `getUser` — Get the authenticated user's Twitter/X profile including ID, name, and username.\n- `createTweet` — Post a tweet. Supports replies and quote tweets. For threads, chain multiple tweets using replyToId.\n- `createThread` — Post a Twitter/X thread (chain of tweets). Each tweet is posted as a reply to the previous one.\n- `getTweetAnalytics` — Get analytics for a single tweet including impressions, likes, replies, retweets, quotes, bookmarks, and engagement rate.\n- `deleteTweet` — Delete a tweet by ID. This action is irreversible.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "getUser",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Get the authenticated user's Twitter/X profile including ID, name, and username.",
        "returnType": "Promise<{ id: string; name: string; username: string; profileImageUrl?: string }>",
        "parameters": [],
        "metadata": {
          "category": "twitter",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve the authenticated Twitter/X user's profile. Returns ID, name, username, and profile image URL.",
          "parameters": [],
          "returns": {
            "type": "Promise<TwitterUser>",
            "description": "The authenticated user's Twitter profile."
          },
          "examples": [
            {
              "title": "Get current user",
              "code": "const user = await twitter.getUser();\nconsole.log(`Posting as @${user.username}`);"
            }
          ]
        }
      },
      {
        "name": "createTweet",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Post a tweet. Supports replies and quote tweets. For threads, chain multiple tweets using replyToId.",
        "returnType": "Promise<{ id: string; url: string }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "replyToId",
            "type": "string",
            "optional": true
          },
          {
            "name": "quoteTweetId",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Post a tweet on Twitter/X. Supports replies and quote tweets. To create a thread, use the returned ID as replyToId for the next tweet.",
          "parameters": [
            {
              "name": "text",
              "type": "string",
              "description": "The tweet text. Max 280 characters for standard accounts, 4000 for premium.",
              "example": "\"Check out our new blog post on AI research!\""
            },
            {
              "name": "replyToId",
              "type": "string",
              "description": "Tweet ID to reply to. Use this to create threads by chaining tweets.",
              "optional": true
            },
            {
              "name": "quoteTweetId",
              "type": "string",
              "description": "Tweet ID to quote tweet.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ id: string; url: string }>",
            "description": "The created tweet ID and URL."
          },
          "examples": [
            {
              "title": "Post a simple tweet",
              "code": "const tweet = await twitter.createTweet({ text: \"Hello from Quokly!\" });"
            },
            {
              "title": "Create a thread",
              "code": "const t1 = await twitter.createTweet({ text: \"🧵 Thread: Why source transparency matters in AI research\" });\nconst t2 = await twitter.createTweet({ text: \"1/ Most AI tools give you answers without showing their work. That's a problem.\", replyToId: t1.id });\nconst t3 = await twitter.createTweet({ text: \"2/ Quokly shows you every source, classifies trust, and gives confidence scores.\", replyToId: t2.id });"
            }
          ]
        }
      },
      {
        "name": "createThread",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Post a Twitter/X thread (chain of tweets). Each tweet is posted as a reply to the previous one.",
        "returnType": "Promise<Array<{ id: string; url: string }>>",
        "parameters": [
          {
            "name": "tweets",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Post a complete Twitter/X thread in one call. Pass an array of tweet texts; they are automatically chained as replies.",
          "parameters": [
            {
              "name": "tweets",
              "type": "string[]",
              "description": "Array of tweet texts to post as a thread.",
              "example": "[\"Thread intro 🧵\", \"Point 1...\", \"Point 2...\", \"Conclusion + link\"]"
            }
          ],
          "returns": {
            "type": "Promise<Array<{ id: string; url: string }>>",
            "description": "Array of created tweet IDs and URLs."
          },
          "examples": [
            {
              "title": "Post a research insight thread",
              "code": "const thread = await twitter.createThread({\n  tweets: [\n    \"🧵 3 things I learned building an AI research tool for Australian startups\",\n    \"1/ Most founders don't verify AI sources. They trust the output blindly.\",\n    \"2/ Confidence scoring changes how you use AI. When the model says '45% confident', you double-check.\",\n    \"3/ Source classification (peer-reviewed vs blog vs social) is table stakes.\",\n    \"We built Quokly to solve all three → https://quokly.com.au\"\n  ]\n});"
            }
          ]
        }
      },
      {
        "name": "getTweetAnalytics",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Get analytics for a single tweet including impressions, likes, replies, retweets, quotes, bookmarks, and engagement rate.",
        "returnType": "Promise<{ impressions: number; likes: number; replies: number; retweets: number; quotes: number; bookmarks: number; engagementRate: number }>",
        "parameters": [
          {
            "name": "tweetId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "readonly",
            "analytics"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve engagement analytics for a single tweet by ID. Returns impressions, likes, replies, retweets, quotes, bookmarks, and calculated engagement rate.",
          "parameters": [
            {
              "name": "tweetId",
              "type": "string",
              "description": "The ID of the tweet to analyze."
            }
          ],
          "returns": {
            "type": "Promise<TwitterTweetAnalytics>",
            "description": "Engagement metrics for the tweet."
          }
        }
      },
      {
        "name": "deleteTweet",
        "requiredCredentials": [
          "twitter-oauth"
        ],
        "description": "Delete a tweet by ID. This action is irreversible.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "tweetId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "twitter",
          "tags": [
            "write",
            "destructive"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Delete a tweet. This action is irreversible.",
          "parameters": [
            {
              "name": "tweetId",
              "type": "string",
              "description": "The ID of the tweet to delete."
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean }>",
            "description": "Confirmation of deletion."
          }
        }
      }
    ]
  },
  {
    "slug": "serper",
    "version": "1.1.7",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/serper/1.1.7/2c2a072291a03bb9c07686fec3ff4d38d051c000659ae2cfed45da9c90719675.mjs",
    "digest": "2c2a072291a03bb9c07686fec3ff4d38d051c000659ae2cfed45da9c90719675",
    "contract_version": 1,
    "display_name": "Serper",
    "description": "Search the web, news and images through Serper.dev for research and enrichment.",
    "category": "Search & Research",
    "auth": {
      "type": "token",
      "provider": "serper",
      "label": "Serper.dev API key",
      "hint": "Create a key at serper.dev."
    },
    "marketplace": {
      "displayName": "Serper",
      "category": "Search & Research",
      "tags": [
        "search",
        "research"
      ],
      "useCases": [
        "Research a topic and synthesize an answer with sources",
        "Monitor news and results for a keyword on a schedule",
        "Scrape pages to enrich a lead or competitor profile",
        "Find images and source links for content workflows"
      ],
      "permissionsSummary": "Read-only web search and page scraping via the Serper.dev API key. No write or destructive actions.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "28cbc844dcec599535c2e93d5dbb8c5366cc6ece",
      "icon_url": "https://orgsdk.ai/plugins/icons/serper.png",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/serper"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/serper/1.1.7/2c2a072291a03bb9c07686fec3ff4d38d051c000659ae2cfed45da9c90719675.notices.txt"
    },
    "documentation": "# Serper plugin\n\nSearch the web, news and images through Serper.dev for research and enrichment.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `search` — Search Google via Serper.dev. Returns organic results, knowledge graph, answer box, people-also-ask, and related searches.\n- `newsSearch` — Search Google News via Serper.dev. Returns news articles matching the query.\n- `imageSearch` — Search Google Images via Serper.dev. Returns image results with thumbnails and source links.\n- `scrapeUrl` — Scrape the full text content and metadata of a web page via the Serper.dev scrape endpoint (scrape.serper.dev). Returns the page's visible text plus Open Graph / Twitter / title / description metadata.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "defaultCountry": {
          "description": "Two-letter country code for search results (e.g. \"us\", \"au\", \"uk\")",
          "type": "string",
          "minLength": 2,
          "maxLength": 2
        },
        "defaultLanguage": {
          "description": "Language code for results (e.g. \"en\", \"es\", \"fr\")",
          "type": "string"
        },
        "defaultNumResults": {
          "description": "Number of results per page (1–100)",
          "type": "integer",
          "minimum": 1,
          "maximum": 100
        },
        "defaultDevice": {
          "description": "Device type for search results",
          "type": "string",
          "enum": [
            "desktop",
            "mobile"
          ]
        }
      },
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "search",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Search Google via Serper.dev. Returns organic results, knowledge graph, answer box, people-also-ask, and related searches.",
        "returnType": "Promise<SearchResult>",
        "typeDeclarations": "\n\t\t\ttype TimeRange = \"past_hour\" | \"past_24h\" | \"past_week\" | \"past_month\" | \"past_year\";\n\t\t\tinterface OrganicResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\tdate?: string;\n\t\t\t\tsitelinks?: Array<{ title: string; link: string }>;\n\t\t\t}\n\t\t\tinterface AnswerBox {\n\t\t\t\tsnippet?: string;\n\t\t\t\ttitle?: string;\n\t\t\t\tlink?: string;\n\t\t\t\tanswer?: string;\n\t\t\t}\n\t\t\tinterface KnowledgeGraph {\n\t\t\t\ttitle?: string;\n\t\t\t\ttype?: string;\n\t\t\t\tdescription?: string;\n\t\t\t\tsourceUrl?: string;\n\t\t\t\tattributes?: Record<string, string>;\n\t\t\t}\n\t\t\tinterface SearchResult {\n\t\t\t\tquery: string;\n\t\t\t\tsearchParameters: { q: string; gl: string; hl?: string; page: number; tbs?: string; };\n\t\t\t\tanswerBox?: AnswerBox;\n\t\t\t\tknowledgeGraph?: KnowledgeGraph;\n\t\t\t\torganic: OrganicResult[];\n\t\t\t\tpeopleAlsoAsk?: Array<{ question: string; snippet?: string; link?: string }>;\n\t\t\t\trelatedSearches?: Array<{ query: string }>;\n\t\t\t}\n\t\t\tinterface ImageResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\timageUrl: string;\n\t\t\t\timageWidth?: number;\n\t\t\t\timageHeight?: number;\n\t\t\t\tsource?: string;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "q",
            "type": "string",
            "description": "The search query."
          },
          {
            "name": "gl",
            "type": "string",
            "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
            "optional": true
          },
          {
            "name": "hl",
            "type": "string",
            "description": "Language code (e.g. \"en\", \"es\").",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "description": "Result page number (1-based). Default: 1.",
            "optional": true
          },
          {
            "name": "num",
            "type": "number",
            "description": "Number of results per page. Default: 10.",
            "optional": true
          },
          {
            "name": "timeRange",
            "type": "TimeRange",
            "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
            "optional": true
          },
          {
            "name": "location",
            "type": "string",
            "description": "Location string (e.g. \"Austin,Texas,United States\").",
            "optional": true
          },
          {
            "name": "device",
            "type": "\"desktop\" | \"mobile\"",
            "description": "Device type for results. Default: desktop.",
            "optional": true
          },
          {
            "name": "autocorrect",
            "type": "boolean",
            "description": "Whether to apply autocorrect. Default: true.",
            "optional": true
          }
        ],
        "metadata": {
          "category": "search",
          "tags": [
            "readonly",
            "search",
            "google",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Search Google using the Serper.dev API. Returns rich results including organic listings, knowledge graph panels, answer boxes, related questions, and more.",
          "parameters": [
            {
              "name": "q",
              "type": "string",
              "description": "The search query.",
              "example": "\"apple inc\""
            },
            {
              "name": "gl",
              "type": "string",
              "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
              "optional": true,
              "defaultValue": "us"
            },
            {
              "name": "hl",
              "type": "string",
              "description": "Language code (e.g. \"en\", \"es\").",
              "optional": true
            },
            {
              "name": "page",
              "type": "number",
              "description": "Result page number (1-based). Default: 1.",
              "optional": true,
              "defaultValue": "1"
            },
            {
              "name": "num",
              "type": "number",
              "description": "Number of results per page. Default: 10.",
              "optional": true,
              "defaultValue": "10"
            },
            {
              "name": "timeRange",
              "type": "TimeRange",
              "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
              "optional": true
            },
            {
              "name": "location",
              "type": "string",
              "description": "Location string (e.g. \"Austin,Texas,United States\").",
              "optional": true
            },
            {
              "name": "device",
              "type": "\"desktop\" | \"mobile\"",
              "description": "Device type for results. Default: desktop.",
              "optional": true
            },
            {
              "name": "autocorrect",
              "type": "boolean",
              "description": "Whether to apply autocorrect. Default: true.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<SearchResult>",
            "description": "Search results including organic listings, knowledge graph, answer box, people-also-ask, and related searches."
          },
          "examples": [
            {
              "title": "Basic search",
              "code": "const results = await serper.search({ q: \"climate change impact\" });\nfor (const r of results.organic) {\n  console.log(`${r.position}. ${r.title}\\n   ${r.link}`);\n}"
            },
            {
              "title": "Search with time range and country",
              "code": "const recent = await serper.search({\n  q: \"AI news\",\n  gl: \"au\",\n  timeRange: \"past_24h\",\n  num: 5,\n});"
            },
            {
              "title": "Paginated search",
              "code": "const page2 = await serper.search({\n  q: \"typescript best practices\",\n  page: 2,\n  num: 20,\n});"
            }
          ]
        }
      },
      {
        "name": "newsSearch",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Search Google News via Serper.dev. Returns news articles matching the query.",
        "returnType": "Promise<SearchResult>",
        "typeDeclarations": "\n\t\t\ttype TimeRange = \"past_hour\" | \"past_24h\" | \"past_week\" | \"past_month\" | \"past_year\";\n\t\t\tinterface OrganicResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\tdate?: string;\n\t\t\t\tsitelinks?: Array<{ title: string; link: string }>;\n\t\t\t}\n\t\t\tinterface AnswerBox {\n\t\t\t\tsnippet?: string;\n\t\t\t\ttitle?: string;\n\t\t\t\tlink?: string;\n\t\t\t\tanswer?: string;\n\t\t\t}\n\t\t\tinterface KnowledgeGraph {\n\t\t\t\ttitle?: string;\n\t\t\t\ttype?: string;\n\t\t\t\tdescription?: string;\n\t\t\t\tsourceUrl?: string;\n\t\t\t\tattributes?: Record<string, string>;\n\t\t\t}\n\t\t\tinterface SearchResult {\n\t\t\t\tquery: string;\n\t\t\t\tsearchParameters: { q: string; gl: string; hl?: string; page: number; tbs?: string; };\n\t\t\t\tanswerBox?: AnswerBox;\n\t\t\t\tknowledgeGraph?: KnowledgeGraph;\n\t\t\t\torganic: OrganicResult[];\n\t\t\t\tpeopleAlsoAsk?: Array<{ question: string; snippet?: string; link?: string }>;\n\t\t\t\trelatedSearches?: Array<{ query: string }>;\n\t\t\t}\n\t\t\tinterface ImageResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\timageUrl: string;\n\t\t\t\timageWidth?: number;\n\t\t\t\timageHeight?: number;\n\t\t\t\tsource?: string;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "q",
            "type": "string",
            "description": "The search query."
          },
          {
            "name": "gl",
            "type": "string",
            "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
            "optional": true
          },
          {
            "name": "hl",
            "type": "string",
            "description": "Language code (e.g. \"en\", \"es\").",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "description": "Result page number (1-based). Default: 1.",
            "optional": true
          },
          {
            "name": "num",
            "type": "number",
            "description": "Number of results per page. Default: 10.",
            "optional": true
          },
          {
            "name": "location",
            "type": "string",
            "description": "Location string (e.g. \"Austin,Texas,United States\").",
            "optional": true
          },
          {
            "name": "device",
            "type": "\"desktop\" | \"mobile\"",
            "description": "Device type for results. Default: desktop.",
            "optional": true
          },
          {
            "name": "autocorrect",
            "type": "boolean",
            "description": "Whether to apply autocorrect. Default: true.",
            "optional": true
          }
        ],
        "metadata": {
          "category": "search",
          "tags": [
            "readonly",
            "search",
            "google",
            "news",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Search Google News using the Serper.dev API. Returns news articles with titles, snippets, dates, and links.",
          "parameters": [
            {
              "name": "q",
              "type": "string",
              "description": "The search query.",
              "example": "\"apple inc\""
            },
            {
              "name": "gl",
              "type": "string",
              "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
              "optional": true,
              "defaultValue": "us"
            },
            {
              "name": "hl",
              "type": "string",
              "description": "Language code (e.g. \"en\", \"es\").",
              "optional": true
            },
            {
              "name": "page",
              "type": "number",
              "description": "Result page number (1-based). Default: 1.",
              "optional": true,
              "defaultValue": "1"
            },
            {
              "name": "num",
              "type": "number",
              "description": "Number of results per page. Default: 10.",
              "optional": true,
              "defaultValue": "10"
            },
            {
              "name": "location",
              "type": "string",
              "description": "Location string (e.g. \"Austin,Texas,United States\").",
              "optional": true
            },
            {
              "name": "device",
              "type": "\"desktop\" | \"mobile\"",
              "description": "Device type for results. Default: desktop.",
              "optional": true
            },
            {
              "name": "autocorrect",
              "type": "boolean",
              "description": "Whether to apply autocorrect. Default: true.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<SearchResult>",
            "description": "News article results."
          },
          "examples": [
            {
              "title": "Search for recent news",
              "code": "const news = await serper.newsSearch({\n  q: \"artificial intelligence\",\n  gl: \"au\",\n});\nfor (const article of news.organic) {\n  console.log(`${article.date} - ${article.title}`);\n}"
            }
          ]
        }
      },
      {
        "name": "imageSearch",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Search Google Images via Serper.dev. Returns image results with thumbnails and source links.",
        "returnType": "Promise<{ query: string; images: ImageResult[] }>",
        "typeDeclarations": "\n\t\t\ttype TimeRange = \"past_hour\" | \"past_24h\" | \"past_week\" | \"past_month\" | \"past_year\";\n\t\t\tinterface OrganicResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\tdate?: string;\n\t\t\t\tsitelinks?: Array<{ title: string; link: string }>;\n\t\t\t}\n\t\t\tinterface AnswerBox {\n\t\t\t\tsnippet?: string;\n\t\t\t\ttitle?: string;\n\t\t\t\tlink?: string;\n\t\t\t\tanswer?: string;\n\t\t\t}\n\t\t\tinterface KnowledgeGraph {\n\t\t\t\ttitle?: string;\n\t\t\t\ttype?: string;\n\t\t\t\tdescription?: string;\n\t\t\t\tsourceUrl?: string;\n\t\t\t\tattributes?: Record<string, string>;\n\t\t\t}\n\t\t\tinterface SearchResult {\n\t\t\t\tquery: string;\n\t\t\t\tsearchParameters: { q: string; gl: string; hl?: string; page: number; tbs?: string; };\n\t\t\t\tanswerBox?: AnswerBox;\n\t\t\t\tknowledgeGraph?: KnowledgeGraph;\n\t\t\t\torganic: OrganicResult[];\n\t\t\t\tpeopleAlsoAsk?: Array<{ question: string; snippet?: string; link?: string }>;\n\t\t\t\trelatedSearches?: Array<{ query: string }>;\n\t\t\t}\n\t\t\tinterface ImageResult {\n\t\t\t\tposition: number;\n\t\t\t\ttitle: string;\n\t\t\t\tlink: string;\n\t\t\t\tsnippet: string;\n\t\t\t\timageUrl: string;\n\t\t\t\timageWidth?: number;\n\t\t\t\timageHeight?: number;\n\t\t\t\tsource?: string;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "q",
            "type": "string",
            "description": "The search query."
          },
          {
            "name": "gl",
            "type": "string",
            "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
            "optional": true
          },
          {
            "name": "hl",
            "type": "string",
            "description": "Language code (e.g. \"en\", \"es\").",
            "optional": true
          },
          {
            "name": "page",
            "type": "number",
            "description": "Result page number (1-based). Default: 1.",
            "optional": true
          },
          {
            "name": "num",
            "type": "number",
            "description": "Number of results per page. Default: 10.",
            "optional": true
          },
          {
            "name": "timeRange",
            "type": "TimeRange",
            "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
            "optional": true
          },
          {
            "name": "location",
            "type": "string",
            "description": "Location string (e.g. \"Austin,Texas,United States\").",
            "optional": true
          },
          {
            "name": "device",
            "type": "\"desktop\" | \"mobile\"",
            "description": "Device type for results. Default: desktop.",
            "optional": true
          },
          {
            "name": "autocorrect",
            "type": "boolean",
            "description": "Whether to apply autocorrect. Default: true.",
            "optional": true
          }
        ],
        "metadata": {
          "category": "search",
          "tags": [
            "readonly",
            "search",
            "google",
            "images",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Search Google Images using the Serper.dev API. Returns image results with titles, thumbnail URLs, dimensions, and source links.",
          "parameters": [
            {
              "name": "q",
              "type": "string",
              "description": "The search query.",
              "example": "\"apple inc\""
            },
            {
              "name": "gl",
              "type": "string",
              "description": "Country code for results (e.g. \"us\", \"au\", \"uk\"). Default: \"us\".",
              "optional": true,
              "defaultValue": "us"
            },
            {
              "name": "hl",
              "type": "string",
              "description": "Language code (e.g. \"en\", \"es\").",
              "optional": true
            },
            {
              "name": "page",
              "type": "number",
              "description": "Result page number (1-based). Default: 1.",
              "optional": true,
              "defaultValue": "1"
            },
            {
              "name": "num",
              "type": "number",
              "description": "Number of results per page. Default: 10.",
              "optional": true,
              "defaultValue": "10"
            },
            {
              "name": "timeRange",
              "type": "TimeRange",
              "description": "Time range filter. One of: \"past_hour\", \"past_24h\", \"past_week\", \"past_month\", \"past_year\".",
              "optional": true
            },
            {
              "name": "location",
              "type": "string",
              "description": "Location string (e.g. \"Austin,Texas,United States\").",
              "optional": true
            },
            {
              "name": "device",
              "type": "\"desktop\" | \"mobile\"",
              "description": "Device type for results. Default: desktop.",
              "optional": true
            },
            {
              "name": "autocorrect",
              "type": "boolean",
              "description": "Whether to apply autocorrect. Default: true.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ query: string; images: ImageResult[] }>",
            "description": "Image search results."
          },
          "examples": [
            {
              "title": "Search for images",
              "code": "const results = await serper.imageSearch({ q: \"solar panels installation\" });\nfor (const img of results.images.slice(0, 5)) {\n  console.log(`${img.title} → ${img.imageUrl}`);\n}"
            }
          ]
        }
      },
      {
        "name": "scrapeUrl",
        "requiredCredentials": [
          "serper"
        ],
        "description": "Scrape the full text content and metadata of a web page via the Serper.dev scrape endpoint (scrape.serper.dev). Returns the page's visible text plus Open Graph / Twitter / title / description metadata.",
        "returnType": "Promise<ScrapeResult>",
        "typeDeclarations": "\n\t\t\tinterface ScrapeResult {\n\t\t\t\t/** Full URL that was scraped. */\n\t\t\t\turl: string;\n\t\t\t\t/** Extracted visible text content of the page. */\n\t\t\t\ttext: string;\n\t\t\t\tmetadata: {\n\t\t\t\t\ttitle?: string;\n\t\t\t\t\tdescription?: string;\n\t\t\t\t\t[key: string]: string | undefined;\n\t\t\t\t};\n\t\t\t\t/** Serper credit cost for the request. */\n\t\t\t\tcredits: number;\n\t\t\t}\n\t\t",
        "parameters": [
          {
            "name": "url",
            "type": "string",
            "description": "The fully-qualified URL of the page to scrape."
          }
        ],
        "metadata": {
          "category": "scrape",
          "tags": [
            "readonly",
            "scrape",
            "web",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Scrape a web page via the Serper.dev scrape endpoint. Returns the page's visible text content plus metadata (title, description, Open Graph, Twitter cards). Useful for reading an article, extracting copy from a landing page, or feeding page content into an LLM.",
          "parameters": [
            {
              "name": "url",
              "type": "string",
              "description": "The fully-qualified URL of the page to scrape.",
              "example": "\"https://orgsdk.ai/\""
            }
          ],
          "returns": {
            "type": "Promise<ScrapeResult>",
            "description": "The page text, metadata, and the credit cost of the request."
          },
          "examples": [
            {
              "title": "Scrape a landing page",
              "code": "const page = await serper.scrapeUrl({ url: \"https://orgsdk.ai/\" });\nconsole.log(page.metadata.title);\nconsole.log(page.text.slice(0, 500));"
            }
          ]
        }
      }
    ]
  },
  {
    "slug": "llm",
    "version": "1.2.4",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/llm/1.2.4/a835765810d8cb9d83a40d1db673e1a697a1edf0fe289c96565e1b1b16808821.mjs",
    "digest": "a835765810d8cb9d83a40d1db673e1a697a1edf0fe289c96565e1b1b16808821",
    "contract_version": 1,
    "display_name": "LLM",
    "description": "Generate, evaluate and summarize text through an OpenAI-compatible model endpoint.",
    "category": "Data & AI",
    "auth": {
      "type": "token",
      "provider": "llm",
      "label": "API Token",
      "hint": "Used as the Bearer token for LLM requests. Changing supported provider settings does not require reconnecting."
    },
    "marketplace": {
      "displayName": "LLM",
      "category": "Data & AI",
      "tags": [
        "ai",
        "generation"
      ],
      "useCases": [
        "Generate and revise drafts with a chosen quality level",
        "Evaluate and classify text with confidence scoring",
        "Extract structured data from unstructured text",
        "Summarize long documents and synthesize researched answers"
      ],
      "permissionsSummary": "Read-only model calls via your API token. No write or destructive actions.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "759a902c3f7b08d583f40820196528e351de0fbd",
      "icon_url": "",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/llm"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/llm/1.2.4/a835765810d8cb9d83a40d1db673e1a697a1edf0fe289c96565e1b1b16808821.notices.txt"
    },
    "documentation": "# LLM plugin\n\nGenerate, evaluate and summarize text through an OpenAI-compatible model endpoint.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `generate` — Generate free-form text using an LLM. Takes a user prompt and an optional system prompt to guide the response style/behavior. Optional quality parameter controls model intelligence: 'high' (default, most capable), 'medium' (balanced), 'low' (fastest). Returns an object { content: string, tokensUsed: number } with the generated text and the token usage for billing.\n- `evalBoolean` — Use AI to evaluate if text matches given criteria with few-shot examples. Returns { result: boolean, confidence: number }. Requires positive and negative examples to disambiguate the classification boundary. Use confidence thresholds: >0.9 auto-process, 0.7-0.9 log for audit, <0.7 flag for human review. Optional quality parameter: 'high' (default), 'medium', 'low'.\n- `evalMultiCategory` — Use AI to categorize text into multiple categories from a comma-separated list. Returns array of category names. Optional quality parameter: 'high' (default), 'medium', 'low'.\n- `evalStructured` — Use AI to extract structured data from text using a Zod schema object. Returns { data } on success, or { error, data: null } on failure. IMPORTANT: Pass a Zod schema object created with z.object({...}), z.string(), z.number(), etc. JSON schema objects or plain object shapes will fail. When defining the schema, add helpful metadata via .description('...') on each field so the model knows the expected format or details (e.g. date formats, specific keywords). Optional quality parameter: 'high' (default), 'medium', 'low'.\n- `querySearch` — Search for information using LLM. Returns {content, results: [{url, title, snippet, source, publishedDate}]}\n- `summarizeText` — Summarize text content using LLM. Returns {summary, originalLength, summaryLength, tokensUsed}\n- `synthesizeAnswer` — Synthesize research findings into a comprehensive answer using LLM. Returns {answer, question, sources, tokensUsed}\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "properties": {
        "provider": {
          "description": "Default provider for model/baseURL. OpenRouter (default) or Ollama Cloud. Does not affect which credential is used.",
          "type": "string",
          "enum": [
            "openrouter",
            "ollama"
          ]
        },
        "model": {
          "description": "Model name (e.g. anthropic/claude-sonnet-4). Leave blank for the default.",
          "type": "string"
        },
        "baseURL": {
          "description": "Custom API endpoint. Overrides the provider default.",
          "type": "string"
        },
        "maxTokens": {
          "description": "Maximum tokens per response",
          "type": "integer",
          "minimum": 1,
          "maximum": 128000
        },
        "temperature": {
          "description": "Sampling temperature (0 = deterministic, 1 = creative)",
          "type": "number",
          "minimum": 0,
          "maximum": 2
        }
      },
      "additionalProperties": false
    },
    "tools": [
      {
        "name": "generate",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Generate free-form text using an LLM. Takes a user prompt and an optional system prompt to guide the response style/behavior. Optional quality parameter controls model intelligence: 'high' (default, most capable), 'medium' (balanced), 'low' (fastest). Returns an object { content: string, tokensUsed: number } with the generated text and the token usage for billing.",
        "returnType": "Promise<{ content: string; tokensUsed: number }>",
        "parameters": [
          {
            "name": "userPrompt",
            "type": "string"
          },
          {
            "name": "systemPrompt",
            "type": "string",
            "optional": true
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "evalBoolean",
            "evalStructured",
            "evalMultiCategory"
          ]
        }
      },
      {
        "name": "evalBoolean",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Use AI to evaluate if text matches given criteria with few-shot examples. Returns { result: boolean, confidence: number }. Requires positive and negative examples to disambiguate the classification boundary. Use confidence thresholds: >0.9 auto-process, 0.7-0.9 log for audit, <0.7 flag for human review. Optional quality parameter: 'high' (default), 'medium', 'low'.",
        "returnType": "Promise<{ result: boolean; confidence: number }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "criteria",
            "type": "string"
          },
          {
            "name": "positiveExample",
            "type": "{ text: string; matches: true; reason?: string }"
          },
          {
            "name": "negativeExample",
            "type": "{ text: string; matches: false; reason?: string }"
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "2.0.0",
          "relatedTo": [
            "evalMultiCategory",
            "evalStructured"
          ]
        }
      },
      {
        "name": "evalMultiCategory",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Use AI to categorize text into multiple categories from a comma-separated list. Returns array of category names. Optional quality parameter: 'high' (default), 'medium', 'low'.",
        "returnType": "Promise<string[]>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "categories",
            "type": "string"
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "evalBoolean",
            "evalStructured"
          ]
        }
      },
      {
        "name": "evalStructured",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Use AI to extract structured data from text using a Zod schema object. Returns { data } on success, or { error, data: null } on failure. IMPORTANT: Pass a Zod schema object created with z.object({...}), z.string(), z.number(), etc. JSON schema objects or plain object shapes will fail. When defining the schema, add helpful metadata via .description('...') on each field so the model knows the expected format or details (e.g. date formats, specific keywords). Optional quality parameter: 'high' (default), 'medium', 'low'.",
        "returnType": "Promise<{ data: Record<string, any> } | { error: string; data: null }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "schema",
            "type": "z.ZodType<any>"
          },
          {
            "name": "quality",
            "type": "'high' | 'medium' | 'low'",
            "optional": true,
            "description": "Model quality tier: 'high' (default), 'medium', or 'low'"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "evalBoolean",
            "evalMultiCategory"
          ]
        },
        "docs": {
          "summary": "Extract structured data from unstructured text using a Zod schema. The LLM reads the text and returns JSON conforming to the provided schema. Always use Zod schemas (z.object, z.string, z.number, etc.) — plain objects or JSON schema will fail.",
          "parameters": [
            {
              "name": "text",
              "type": "string",
              "description": "The unstructured text to extract data from (email body, document, etc.).",
              "example": "\"Invoice #42 from Acme Corp. Total: $150.00 USD. Due: 2026-03-15.\""
            },
            {
              "name": "schema",
              "type": "Zod schema",
              "description": "A Zod schema object defining the shape of the extracted data. Use .describe() on fields to guide the model on expected format or details.",
              "example": "z.object({ provider: z.string().describe(\"Company name\"), amount: z.number(), currency: z.string().describe(\"3-letter code like USD\") })"
            },
            {
              "name": "quality",
              "type": "'high' | 'medium' | 'low'",
              "description": "Model quality tier. 'high' (default) uses the most capable model, 'medium' balances quality and speed, 'low' is fastest. Omit for default (high).",
              "example": "\"medium\""
            }
          ],
          "returns": {
            "type": "Promise<{ data: Record<string, any> } | { error: string; data: null }>",
            "description": "Always an object with a `data` field. On success, `data` is the extracted value matching the schema. On failure, `data` is null and `error` describes the problem.",
            "example": "{ data: { provider: \"Acme Corp\", amount: 150, currency: \"USD\" } }"
          },
          "examples": [
            {
              "title": "Extract invoice data",
              "code": "const schema = z.object({\n  provider: z.string().describe(\"Company or service name\"),\n  amount: z.number().describe(\"Total amount\"),\n  currency: z.string().describe(\"3-letter currency code\"),\n  dueDate: z.string().optional().describe(\"Due date in YYYY-MM-DD\"),\n});\nconst result = await evalStructured(emailBody, schema);\nif (result.data === null) throw new Error(result.error);\nconst invoice = result.data;"
            },
            {
              "title": "Extract with lower quality for speed",
              "code": "const schema = z.object({\n  name: z.string(),\n  email: z.string(),\n});\nconst { data: contact } = await evalStructured(text, schema, \"low\");"
            },
            {
              "title": "Extract multiple items from text",
              "code": "const schema = z.object({\n  items: z.array(z.object({\n    name: z.string(),\n    quantity: z.number(),\n    price: z.number(),\n  })),\n  total: z.number(),\n});\nconst { data: receipt } = await evalStructured(receiptText, schema);"
            }
          ],
          "caveats": [
            "Must pass a Zod schema (z.object({...})), NOT a plain object or JSON schema",
            "Use .describe() on schema fields to guide extraction — the model uses these as hints",
            "Always unwrap with `result.data`. On failure returns { error, data: null } instead of throwing — check data !== null before using",
            "The z object is pre-loaded in the VM — do not import it"
          ],
          "seeAlso": [
            "evalBoolean",
            "evalMultiCategory"
          ]
        }
      },
      {
        "name": "querySearch",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Search for information using LLM. Returns {content, results: [{url, title, snippet, source, publishedDate}]}",
        "returnType": "Promise<{content: string, results: Array<{url: string, title: string, snippet: string, source: string, publishedDate: string}>}>",
        "parameters": [
          {
            "name": "query",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "network",
            "llm",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "2.0.0",
          "relatedTo": [
            "summarizeText",
            "synthesizeAnswer"
          ]
        }
      },
      {
        "name": "summarizeText",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Summarize text content using LLM. Returns {summary, originalLength, summaryLength, tokensUsed}",
        "returnType": "Promise<{summary: string, originalLength: number, summaryLength: number, tokensUsed: number}>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "maxLength",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "2.0.0",
          "relatedTo": [
            "querySearch",
            "synthesizeAnswer"
          ]
        }
      },
      {
        "name": "synthesizeAnswer",
        "requiredCredentials": [
          "llm",
          "openrouter",
          "ollama"
        ],
        "description": "Synthesize research findings into a comprehensive answer using LLM. Returns {answer, question, sources, tokensUsed}",
        "returnType": "Promise<{answer: string, question: string, sources: string[], tokensUsed: number}>",
        "parameters": [
          {
            "name": "question",
            "type": "string"
          },
          {
            "name": "findings",
            "type": "any[]"
          }
        ],
        "metadata": {
          "category": "llm",
          "tags": [
            "readonly",
            "llm",
            "data"
          ],
          "readonly": true,
          "version": "2.0.0",
          "relatedTo": [
            "querySearch",
            "summarizeText"
          ]
        }
      }
    ]
  },
  {
    "slug": "linkedin",
    "version": "1.0.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/linkedin/1.0.2/2180eeb21ae59d9063dc9583cbf0d6fbaab0135ce23b249cbce5f17d88aac740.mjs",
    "digest": "2180eeb21ae59d9063dc9583cbf0d6fbaab0135ce23b249cbce5f17d88aac740",
    "contract_version": 1,
    "display_name": "LinkedIn",
    "description": "Publish personal and organization posts on LinkedIn and inspect engagement.",
    "category": "Social",
    "auth": {
      "type": "oauth2",
      "provider": "linkedin-oauth",
      "scopes": [
        "r_liteprofile",
        "r_emailaddress",
        "w_member_social",
        "r_organization_social",
        "w_organization_social",
        "r_organization_admin"
      ],
      "label": "LinkedIn OAuth",
      "hint": "Use your own provider OAuth application. Connect its client ID and secret in OrgSDK, then select that credential for this project."
    },
    "marketplace": {
      "displayName": "LinkedIn",
      "category": "Social",
      "tags": [
        "social",
        "posting",
        "analytics"
      ],
      "useCases": [
        "Publish posts to a personal profile or organization page",
        "Schedule a content queue across pages",
        "Track post engagement and impressions",
        "Manage organization content as an admin"
      ],
      "permissionsSummary": "Post, delete, and read analytics for LinkedIn content via OAuth. Posting and deleting are public, mutating actions; deletes are irreversible.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "2ea71fe51927c5dfe20f670ee92982a745f84d1d",
      "icon_url": "https://orgsdk.ai/plugins/icons/linkedin.png",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/linkedin"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/linkedin/1.0.2/2180eeb21ae59d9063dc9583cbf0d6fbaab0135ce23b249cbce5f17d88aac740.notices.txt"
    },
    "documentation": "# LinkedIn plugin\n\nPublish personal and organization posts on LinkedIn and inspect engagement.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `getProfile` — Get the authenticated user's LinkedIn profile including ID, URN, name, and headline.\n- `getOrganizations` — List LinkedIn organization pages where the authenticated user is an administrator.\n- `createPost` — Create a LinkedIn post with optional link attachment. Posts as the authenticated user by default, or as an organization if authorUrn is provided.\n- `getPostAnalytics` — Get analytics (impressions, likes, comments, reposts, clicks, engagement rate) for a LinkedIn post.\n- `deletePost` — Delete a LinkedIn post by its URN.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "getProfile",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Get the authenticated user's LinkedIn profile including ID, URN, name, and headline.",
        "returnType": "Promise<{ id: string; urn: string; name: string; headline?: string; profilePicture?: string }>",
        "parameters": [],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve the authenticated LinkedIn user's profile. Returns ID, URN (needed for posting), name, headline, and profile picture URL.",
          "parameters": [],
          "returns": {
            "type": "Promise<LinkedInProfile>",
            "description": "The authenticated user's LinkedIn profile.",
            "example": "{ id: \"abc123\", urn: \"urn:li:person:abc123\", name: \"Jane Doe\", headline: \"CTO at Example\" }"
          },
          "examples": [
            {
              "title": "Get current user profile",
              "code": "const profile = await linkedin.getProfile();\nconsole.log(`Posting as: ${profile.name}`);"
            }
          ]
        }
      },
      {
        "name": "getOrganizations",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "List LinkedIn organization pages where the authenticated user is an administrator.",
        "returnType": "Promise<Array<{ id: string; urn: string; name: string; logo?: string; vanityName?: string }>>",
        "parameters": [],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "readonly",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "List all LinkedIn organization (company) pages where the authenticated user has admin access. Returns org URN needed for posting as an organization.",
          "parameters": [],
          "returns": {
            "type": "Promise<LinkedInOrganization[]>",
            "description": "Array of organization objects with URN, name, and logo."
          },
          "examples": [
            {
              "title": "Find company page URN",
              "code": "const orgs = await linkedin.getOrganizations();\nconst quokly = orgs.find(o => o.name.includes('Quokly'));"
            }
          ]
        }
      },
      {
        "name": "createPost",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Create a LinkedIn post with optional link attachment. Posts as the authenticated user by default, or as an organization if authorUrn is provided.",
        "returnType": "Promise<{ id: string; url: string }>",
        "parameters": [
          {
            "name": "text",
            "type": "string"
          },
          {
            "name": "authorUrn",
            "type": "string",
            "optional": true
          },
          {
            "name": "visibility",
            "type": "\"PUBLIC\" | \"CONNECTIONS\"",
            "optional": true
          },
          {
            "name": "linkUrl",
            "type": "string",
            "optional": true
          },
          {
            "name": "linkTitle",
            "type": "string",
            "optional": true
          },
          {
            "name": "linkDescription",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Publish a post on LinkedIn. Supports text-only posts and link/article posts with a preview card. By default posts as the authenticated user; pass an organization URN to post as a company page.",
          "parameters": [
            {
              "name": "text",
              "type": "string",
              "description": "The post text content. LinkedIn supports up to 3000 characters.",
              "example": "\"Check out our latest blog post on AI research transparency!\""
            },
            {
              "name": "authorUrn",
              "type": "string",
              "description": "URN of the author. Defaults to the authenticated user's personal profile. Use getOrganizations() to get org URNs.",
              "optional": true,
              "example": "\"urn:li:organization:12345\""
            },
            {
              "name": "visibility",
              "type": "\"PUBLIC\" | \"CONNECTIONS\"",
              "description": "Post visibility. Defaults to PUBLIC.",
              "optional": true,
              "defaultValue": "PUBLIC"
            },
            {
              "name": "linkUrl",
              "type": "string",
              "description": "URL for an article/link attachment. Creates a preview card on the post.",
              "optional": true,
              "example": "\"https://quokly.com.au/blog/my-post\""
            },
            {
              "name": "linkTitle",
              "type": "string",
              "description": "Title for the link preview card.",
              "optional": true
            },
            {
              "name": "linkDescription",
              "type": "string",
              "description": "Description for the link preview card.",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ id: string; url: string }>",
            "description": "The created post ID and LinkedIn URL."
          },
          "examples": [
            {
              "title": "Share a blog post link",
              "code": "const result = await linkedin.createPost({\n  text: \"New on the blog: Why source transparency matters in AI research\",\n  linkUrl: \"https://quokly.com.au/blog/why-source-transparency\",\n  linkTitle: \"Why Source Transparency Matters in AI Research\",\n  linkDescription: \"Learn how verified sources improve AI-powered research.\"\n});"
            },
            {
              "title": "Post as company page",
              "code": "const orgs = await linkedin.getOrganizations();\nconst companyUrn = orgs.find(o => o.name.includes('Quokly')).urn;\nawait linkedin.createPost({\n  text: \"Quokly helps founders research with confidence.\",\n  authorUrn: companyUrn,\n});"
            }
          ]
        }
      },
      {
        "name": "getPostAnalytics",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Get analytics (impressions, likes, comments, reposts, clicks, engagement rate) for a LinkedIn post.",
        "returnType": "Promise<{ impressions: number; uniqueImpressions: number; likes: number; comments: number; reposts: number; clicks: number; engagementRate: number }>",
        "parameters": [
          {
            "name": "postUrn",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "readonly",
            "analytics"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve engagement analytics for a LinkedIn post by its URN. Returns impressions, likes, comments, reposts, clicks, and calculated engagement rate.",
          "parameters": [
            {
              "name": "postUrn",
              "type": "string",
              "description": "The URN of the LinkedIn post to analyze.",
              "example": "\"urn:li:activity:123456789\""
            }
          ],
          "returns": {
            "type": "Promise<LinkedInPostAnalytics>",
            "description": "Engagement metrics for the post."
          }
        }
      },
      {
        "name": "deletePost",
        "requiredCredentials": [
          "linkedin-oauth"
        ],
        "description": "Delete a LinkedIn post by its URN.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "postUrn",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "linkedin",
          "tags": [
            "write",
            "destructive"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Delete a LinkedIn post. This action is irreversible.",
          "parameters": [
            {
              "name": "postUrn",
              "type": "string",
              "description": "The URN of the post to delete."
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean }>",
            "description": "Confirmation of deletion."
          }
        }
      }
    ]
  },
  {
    "slug": "google-tasks",
    "version": "1.0.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/google-tasks/1.0.2/c92ae92ec017d840b312f76140102bdf8b2cfbf602253e8833c5a01c3ef7f365.mjs",
    "digest": "c92ae92ec017d840b312f76140102bdf8b2cfbf602253e8833c5a01c3ef7f365",
    "contract_version": 1,
    "display_name": "Google Tasks",
    "description": "Create, update and complete Google Tasks and manage task lists.",
    "category": "Productivity",
    "auth": {
      "type": "oauth2",
      "provider": "google-oauth",
      "scopes": [
        "https://www.googleapis.com/auth/tasks"
      ],
      "label": "Google Tasks OAuth",
      "hint": "Use your own provider OAuth application. Connect its client ID and secret in OrgSDK, then select that credential for this project."
    },
    "marketplace": {
      "displayName": "Google Tasks",
      "category": "Productivity",
      "tags": [
        "tasks",
        "google",
        "productivity"
      ],
      "useCases": [
        "Create tasks from emails, issues, or project observations",
        "Mark tasks complete and clear finished items on a schedule",
        "Sync a task list to a sheet or report",
        "Track follow-ups with due dates and notes"
      ],
      "permissionsSummary": "Read, create, update, complete, and delete Google Tasks and task lists via OAuth. Reads retrieve data; creates, updates, and deletes are mutating.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "79f514d2bc3925d7f4c0ffa27fb0ad60a186e00b",
      "icon_url": "https://orgsdk.ai/plugins/icons/google-tasks.svg",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/google-tasks"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/google-tasks/1.0.2/c92ae92ec017d840b312f76140102bdf8b2cfbf602253e8833c5a01c3ef7f365.notices.txt"
    },
    "documentation": "# Google Tasks plugin\n\nCreate, update and complete Google Tasks and manage task lists.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `listTaskLists` — List all Google Task lists for the authenticated user.\n- `createTaskList` — Create a new Google Task list.\n- `deleteTaskList` — Delete a Google Task list by ID.\n- `listTasks` — List tasks in a Google Task list. Options: showCompleted (default true), showHidden, dueMin/dueMax (RFC 3339), maxResults (default 100).\n- `getTask` — Get a single Google Task by ID.\n- `createTask` — Create a task in a Google Task list. Options: notes, due (RFC 3339 date string), parent (task ID for subtasks).\n- `updateTask` — Update a Google Task. Pass an updates object with any of: title, notes, due (RFC 3339), status (\"needsAction\" or \"completed\").\n- `completeTask` — Mark a Google Task as completed (sets status to \"completed\").\n- `uncompleteTask` — Mark a Google Task as incomplete (sets status back to \"needsAction\").\n- `deleteTask` — Delete a Google Task by ID.\n- `clearCompletedTasks` — Clear all completed tasks from a Google Task list.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "listTaskLists",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "List all Google Task lists for the authenticated user.",
        "returnType": "Promise<Array<{ id: string; title: string; updated?: string }>>",
        "parameters": [],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "createTaskList",
            "listTasks"
          ]
        }
      },
      {
        "name": "createTaskList",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Create a new Google Task list.",
        "returnType": "Promise<{ id: string; title: string; updated?: string }>",
        "parameters": [
          {
            "name": "title",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listTaskLists",
            "deleteTaskList"
          ]
        }
      },
      {
        "name": "deleteTaskList",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Delete a Google Task list by ID.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listTaskLists",
            "createTaskList"
          ]
        }
      },
      {
        "name": "listTasks",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "List tasks in a Google Task list. Options: showCompleted (default true), showHidden, dueMin/dueMax (RFC 3339), maxResults (default 100).",
        "returnType": "Promise<Array<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "options",
            "type": "{ showCompleted?: boolean; showHidden?: boolean; dueMin?: string; dueMax?: string; maxResults?: number }"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getTask",
            "createTask",
            "listTaskLists"
          ]
        }
      },
      {
        "name": "getTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Get a single Google Task by ID.",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "listTasks",
            "updateTask"
          ]
        }
      },
      {
        "name": "createTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Create a task in a Google Task list. Options: notes, due (RFC 3339 date string), parent (task ID for subtasks).",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "title",
            "type": "string"
          },
          {
            "name": "options",
            "type": "{ notes?: string; due?: string; parent?: string }"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listTasks",
            "updateTask",
            "completeTask",
            "deleteTask"
          ]
        }
      },
      {
        "name": "updateTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Update a Google Task. Pass an updates object with any of: title, notes, due (RFC 3339), status (\"needsAction\" or \"completed\").",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          },
          {
            "name": "updates",
            "type": "{ title?: string; notes?: string; due?: string; status?: \"needsAction\" | \"completed\" }"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "getTask",
            "completeTask",
            "uncompleteTask"
          ]
        }
      },
      {
        "name": "completeTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark a Google Task as completed (sets status to \"completed\").",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "uncompleteTask",
            "updateTask",
            "clearCompletedTasks"
          ]
        }
      },
      {
        "name": "uncompleteTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark a Google Task as incomplete (sets status back to \"needsAction\").",
        "returnType": "Promise<{ id: string; title: string; status: string; notes?: string; due?: string; completed?: string; parent?: string; position?: string; updated?: string }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "completeTask",
            "updateTask"
          ]
        }
      },
      {
        "name": "deleteTask",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Delete a Google Task by ID.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          },
          {
            "name": "taskId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "createTask",
            "listTasks"
          ]
        }
      },
      {
        "name": "clearCompletedTasks",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Clear all completed tasks from a Google Task list.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "taskListId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "io",
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "completeTask",
            "listTasks"
          ]
        }
      }
    ]
  },
  {
    "slug": "google-sheets",
    "version": "1.0.3",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/google-sheets/1.0.3/f6451d8f181752540759d9b602ee3e27b397c57c1a08cdd993f5dcd348f11ac5.mjs",
    "digest": "f6451d8f181752540759d9b602ee3e27b397c57c1a08cdd993f5dcd348f11ac5",
    "contract_version": 1,
    "display_name": "Google Sheets",
    "description": "Read spreadsheet ranges, append rows and update Google Sheets from project actions.",
    "category": "Data & AI",
    "auth": {
      "type": "oauth2",
      "provider": "google-oauth",
      "scopes": [
        "https://www.googleapis.com/auth/spreadsheets",
        "https://www.googleapis.com/auth/tasks"
      ],
      "label": "Google Sheets OAuth",
      "hint": "Use your own provider OAuth application. Connect its client ID and secret in OrgSDK, then select that credential for this project."
    },
    "marketplace": {
      "displayName": "Google Sheets",
      "category": "Data & AI",
      "tags": [
        "spreadsheets",
        "google",
        "data"
      ],
      "useCases": [
        "Sync CRM or form data into a structured sheet table",
        "Generate weekly reports by appending action run results",
        "Update specific rows in a table from workflow output",
        "Read a sheet as a config or lookup source for a project"
      ],
      "permissionsSummary": "Access Google Sheets and Tasks data via OAuth.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "e9717eee88eac7d8d68e35d96ce6489e6b01823d",
      "icon_url": "https://orgsdk.ai/plugins/icons/google-sheets.svg",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/google-sheets"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/google-sheets/1.0.3/f6451d8f181752540759d9b602ee3e27b397c57c1a08cdd993f5dcd348f11ac5.notices.txt"
    },
    "documentation": "# Google Sheets plugin\n\nRead spreadsheet ranges, append rows and update Google Sheets from project actions.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `appendToSheet` — Append rows to a Google Sheet range. Always pass a 2D array: [[val1, val2], ...]. Spreadsheet ID + range are passed at runtime.\n- `getSheetData` — Read values from a Google Sheet range\n- `clearSheet` — Clear values in a Google Sheet range. Tip: use a range that excludes headers (e.g. Sheet1!A2:Z)\n- `listSheetTables` — List all structured tables in a Google Sheet. Returns array with tableId (string), name, sheetId, sheetTitle, range (GridRange), a1Range (A1 notation for debugging).\n- `appendToTable` — Append rows to a structured Google Sheet table. Pass rows as objects keyed by column name: [{ Provider: 'X', Amount: 100 }, ...]. Column names must match table headers exactly. Use listSheetTables to find tableId. Formula detection: strings starting with '='.\n- `getTableData` — Read all data rows from a structured Google Sheet table. Returns rows as objects with column names as keys. Each row includes _rowIndex for updates. Use listSheetTables to find tableId.\n- `updateTableRow` — Update specific columns in a row of a Google Sheet table. Use _rowIndex from getTableData. Pass updates as { columnName: newValue }.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "appendToSheet",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "getSheetData",
            "clearSheet",
            "appendToTable"
          ]
        },
        "description": "Append rows to a Google Sheet range. Always pass a 2D array: [[val1, val2], ...]. Spreadsheet ID + range are passed at runtime.",
        "returnType": "Promise<{ success: boolean; updates: any }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "range",
            "type": "string"
          },
          {
            "name": "rows",
            "type": "unknown[][]"
          },
          {
            "name": "valueInputOption",
            "type": "\"RAW\" | \"USER_ENTERED\""
          }
        ],
        "docs": {
          "summary": "Append rows to a Google Sheet at a specified range. Rows are inserted after the last row of data in the range. Accepts a full Sheets URL or just the spreadsheet ID.",
          "parameters": [
            {
              "name": "spreadsheetId",
              "type": "string",
              "description": "The spreadsheet ID from the Google Sheets URL, or the full URL itself (the ID will be extracted automatically).",
              "example": "\"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms\""
            },
            {
              "name": "range",
              "type": "string",
              "description": "A1 notation range where rows will be appended. Typically the sheet name or sheet + column range.",
              "example": "\"Sheet1!A:E\""
            },
            {
              "name": "rows",
              "type": "unknown[][]",
              "description": "2D array of values to append. Each inner array is one row. Values can be strings, numbers, booleans, or null.",
              "example": "[[\"Acme Corp\", 150, \"USD\", \"2026-03-15\"]]"
            },
            {
              "name": "valueInputOption",
              "type": "\"RAW\" | \"USER_ENTERED\"",
              "description": "How values are interpreted. USER_ENTERED parses formulas and dates like the Sheets UI. RAW inserts values as-is.",
              "defaultValue": "USER_ENTERED",
              "optional": true
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean; updates: any }>",
            "description": "Success status and update metadata from the Sheets API."
          },
          "examples": [
            {
              "title": "Append a single row",
              "code": "await googleSheets.appendToSheet(\n  \"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms\",\n  \"Sheet1!A:D\",\n  [[\"Acme Corp\", 150, \"USD\", \"2026-03-15\"]]\n);"
            },
            {
              "title": "Append multiple rows",
              "code": "const rows = invoices.map(inv => [\n  inv.provider,\n  inv.amount,\n  inv.currency,\n  new Date().toISOString().split(\"T\")[0],\n]);\nawait googleSheets.appendToSheet(SPREADSHEET_ID, \"Expenses!A:D\", rows);",
              "description": "Batch append from processed data"
            }
          ],
          "caveats": [
            "Rows must be a 2D array — [[val1, val2], ...], not an array of objects",
            "For object-based rows, use appendToTable instead (requires a structured table)",
            "Column order must match the sheet headers — verify with getSheetData first"
          ],
          "seeAlso": [
            "getSheetData",
            "clearSheet",
            "appendToTable"
          ]
        }
      },
      {
        "name": "getSheetData",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "appendToSheet",
            "listSheetTables"
          ]
        },
        "description": "Read values from a Google Sheet range",
        "returnType": "Promise<unknown[][]>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "range",
            "type": "string"
          }
        ]
      },
      {
        "name": "clearSheet",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "appendToSheet",
            "getSheetData"
          ]
        },
        "description": "Clear values in a Google Sheet range. Tip: use a range that excludes headers (e.g. Sheet1!A2:Z)",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "range",
            "type": "string"
          }
        ]
      },
      {
        "name": "listSheetTables",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "appendToTable",
            "getSheetData"
          ]
        },
        "description": "List all structured tables in a Google Sheet. Returns array with tableId (string), name, sheetId, sheetTitle, range (GridRange), a1Range (A1 notation for debugging).",
        "returnType": "Promise<Array<{ tableId: string; name: string; sheetId: number; sheetTitle: string; range: any; a1Range: string }>>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          }
        ]
      },
      {
        "name": "appendToTable",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "listSheetTables",
            "appendToSheet"
          ]
        },
        "description": "Append rows to a structured Google Sheet table. Pass rows as objects keyed by column name: [{ Provider: 'X', Amount: 100 }, ...]. Column names must match table headers exactly. Use listSheetTables to find tableId. Formula detection: strings starting with '='.",
        "returnType": "Promise<{ success: boolean; updates: any }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "tableId",
            "type": "string"
          },
          {
            "name": "rows",
            "type": "Record<string, unknown>[]"
          }
        ]
      },
      {
        "name": "getTableData",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "listSheetTables",
            "appendToTable",
            "updateTableRow"
          ]
        },
        "description": "Read all data rows from a structured Google Sheet table. Returns rows as objects with column names as keys. Each row includes _rowIndex for updates. Use listSheetTables to find tableId.",
        "returnType": "Promise<{ tableId: string; columns: string[]; rows: Array<Record<string, unknown>>; rowCount: number }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "tableId",
            "type": "string"
          }
        ]
      },
      {
        "name": "updateTableRow",
        "requiredCredentials": [
          "google-oauth"
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "readonly": false,
          "version": "1.0.0",
          "relatedTo": [
            "getTableData",
            "appendToTable"
          ]
        },
        "description": "Update specific columns in a row of a Google Sheet table. Use _rowIndex from getTableData. Pass updates as { columnName: newValue }.",
        "returnType": "Promise<{ success: boolean; updatedCells: number; updatedColumns: string[] }>",
        "parameters": [
          {
            "name": "spreadsheetId",
            "type": "string"
          },
          {
            "name": "tableId",
            "type": "string"
          },
          {
            "name": "rowIndex",
            "type": "number"
          },
          {
            "name": "updates",
            "type": "Record<string, unknown>"
          }
        ]
      }
    ]
  },
  {
    "slug": "gmail",
    "version": "1.0.6",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/gmail/1.0.6/c47e6d678ec6056fbbb634a8f818bc020742782f71bc02077e4480cfb0297ab3.mjs",
    "digest": "c47e6d678ec6056fbbb634a8f818bc020742782f71bc02077e4480cfb0297ab3",
    "contract_version": 1,
    "display_name": "Gmail",
    "description": "Search email, work with attachments, draft replies and send messages through Gmail.",
    "category": "Communication",
    "auth": {
      "type": "oauth2",
      "provider": "google-oauth",
      "scopes": [
        "https://mail.google.com/"
      ],
      "label": "Gmail OAuth",
      "hint": "Use your own provider OAuth application. Connect its client ID and secret in OrgSDK, then select that credential for this project."
    },
    "marketplace": {
      "displayName": "Gmail",
      "category": "Communication",
      "tags": [
        "email",
        "google"
      ],
      "useCases": [
        "Triage a label by priority and surface threads that need a reply",
        "Draft and send replies that match the thread context",
        "Extract attachments and parse PDFs (e.g. invoices, expense reports)",
        "Route receipt threads to a Google Sheet and mark them processed",
        "Send templated notifications or reports with attachments"
      ],
      "permissionsSummary": "Read, modify labels on, and send Gmail messages via a restricted OAuth scope. Reading and labeling are reversible; sending and replying are mutating.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "2e0f6911e09c1e43fdb921be2492379efe9a3845",
      "icon_url": "https://orgsdk.ai/plugins/icons/gmail.svg",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/gmail"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/gmail/1.0.6/c47e6d678ec6056fbbb634a8f818bc020742782f71bc02077e4480cfb0297ab3.notices.txt"
    },
    "documentation": "# Gmail plugin\n\nSearch email, work with attachments, draft replies and send messages through Gmail.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `getMessages` — Fetch Gmail messages using Gmail search query syntax (q). Label filters must use the label name (not the label ID), e.g. label:Expense or label:\"Expense Reports\". Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.\n- `getMessage` — Fetch a single Gmail message by ID. Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.\n- `listLabels` — List Gmail labels (id + name)\n- `ensureLabel` — Get or create a Gmail label by name; returns label object (id + name)\n- `modifyMessageLabels` — Add/remove Gmail label IDs on a message. Use removeLabelIds:['UNREAD'] to mark as read.\n- `batchModifyMessages` — Batch add/remove label IDs across multiple message IDs\n- `markMessageAsRead` — Mark Gmail message as read (removes UNREAD label)\n- `markMessageAsUnread` — Mark Gmail message as unread (adds UNREAD label)\n- `sendMessage` — Send an email via Gmail. Supports plain-text or HTML body. When html is provided, sends a multipart/alternative message with both text/plain and text/html parts.\n- `sendMessageWithAttachment` — Send an email with file attachments via Gmail. Builds a MIME multipart/mixed message with a plain-text or HTML body and one or more base64-encoded attachments. When html is provided, the body is nested in multipart/alternative.\n- `replyToMessage` — Reply to a Gmail message within its thread. Fetches the original message to set threading headers (In-Reply-To, References) and replies to the original sender. Supports HTML via multipart/alternative when html parameter is provided.\n- `getMessageAttachment` — Fetch a Gmail attachment by messageId + attachmentId. Returns standard base64 bytes (dataBase64).\n- `getThread` — Fetch all messages in a Gmail thread by threadId, in chronological order. Unlike getMessages, this preserves quoted reply chains so the full conversation context is available. Use this before replying to understand the complete thread history.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "getMessages",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch Gmail messages using Gmail search query syntax (q). Label filters must use the label name (not the label ID), e.g. label:Expense or label:\"Expense Reports\". Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.",
        "returnType": "Promise<Array<{ id: string; threadId?: string; subject?: string; from?: string; date?: string; body: string; headers?: Record<string, string>; labelIds?: string[]; attachments?: Array<{ filename: string; mimeType?: string; attachmentId: string; size?: number }> }>>",
        "parameters": [
          {
            "name": "query",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessage",
            "listLabels"
          ]
        },
        "docs": {
          "summary": "Fetch Gmail messages using Gmail search query syntax. Returns cleaned text bodies optimized for LLM processing — HTML is converted to markdown, tracking pixels and quoted reply chains are removed.",
          "parameters": [
            {
              "name": "query",
              "type": "string",
              "description": "Gmail search query syntax. Label filters must use label name (not ID), e.g. label:Expense. Supports standard operators: from:, to:, subject:, after:, before:, is:unread, has:attachment.",
              "example": "\"label:Expense is:unread after:2026/01/01\"",
              "optional": true
            },
            {
              "name": "maxResults",
              "type": "number",
              "description": "Maximum number of messages to return.",
              "defaultValue": "10",
              "optional": true,
              "example": "20"
            }
          ],
          "returns": {
            "type": "Promise<GmailMessage[]>",
            "description": "Array of message objects with id, subject, from, date, body (cleaned text), headers, labelIds, and attachments.",
            "example": "[{ id: \"18f...\", subject: \"Invoice #42\", from: \"vendor@example.com\", body: \"Please find attached...\", attachments: [{ filename: \"invoice.pdf\", attachmentId: \"ANGj...\" }] }]"
          },
          "examples": [
            {
              "title": "Fetch recent unread emails",
              "code": "const msgs = await gmail.getMessages(\"is:unread\", 5);\nmsgs.map(m => ({ subject: m.subject, from: m.from }));"
            },
            {
              "title": "Search by label and date",
              "code": "const invoices = await gmail.getMessages(\"label:Invoices after:2026/01/01\", 50);"
            },
            {
              "title": "Filter with LLM classification",
              "code": "const msgs = await gmail.getMessages(\"label:inbox\", 20);\nconst expenses = [];\nfor (const m of msgs) {\n  const isExpense = await llm.evalBoolean(\n    m.body,\n    \"is an expense receipt or invoice\",\n    { text: \"Your payment of $50 was processed\", matches: true },\n    { text: \"Meeting tomorrow at 3pm\", matches: false }\n  );\n  if (isExpense.result) expenses.push(m);\n}\nexpenses;",
              "description": "Combine with llm.evalBoolean to classify emails"
            }
          ],
          "caveats": [
            "Use label names (not IDs) in query filters — e.g. label:Expense, not label:Label_13",
            "Messages are fetched sequentially; limit maxResults to 20-50 to stay within timeout",
            "Quoted reply chains are truncated — only the first/main message body is returned"
          ],
          "seeAlso": [
            "getMessage",
            "listLabels",
            "modifyMessageLabels"
          ]
        }
      },
      {
        "name": "getMessage",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch a single Gmail message by ID. Returns cleaned text body optimized for LLM processing. HTML is converted to clean text, tracking pixels and quoted chains are removed.",
        "returnType": "Promise<{ id: string; threadId?: string; subject?: string; from?: string; date?: string; body: string; headers?: Record<string, string>; labelIds?: string[]; attachments?: Array<{ filename: string; mimeType?: string; attachmentId: string; size?: number }> }>",
        "parameters": [
          {
            "name": "id",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessages",
            "getMessageAttachment"
          ]
        }
      },
      {
        "name": "listLabels",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "List Gmail labels (id + name)",
        "returnType": "Promise<Array<{ id?: string; name?: string }>>",
        "parameters": [],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "ensureLabel",
            "getMessages"
          ]
        }
      },
      {
        "name": "ensureLabel",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Get or create a Gmail label by name; returns label object (id + name)",
        "returnType": "Promise<{ id?: string; name?: string }>",
        "parameters": [
          {
            "name": "name",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "listLabels",
            "modifyMessageLabels"
          ]
        }
      },
      {
        "name": "modifyMessageLabels",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Add/remove Gmail label IDs on a message. Use removeLabelIds:['UNREAD'] to mark as read.",
        "returnType": "Promise<{ success: boolean; id?: string; labelIds: string[] }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          },
          {
            "name": "addLabelIds",
            "type": "string[]"
          },
          {
            "name": "removeLabelIds",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "ensureLabel",
            "markMessageAsRead",
            "markMessageAsUnread"
          ]
        }
      },
      {
        "name": "batchModifyMessages",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Batch add/remove label IDs across multiple message IDs",
        "returnType": "Promise<{ success: boolean; count: number }>",
        "parameters": [
          {
            "name": "messageIds",
            "type": "string[]"
          },
          {
            "name": "addLabelIds",
            "type": "string[]"
          },
          {
            "name": "removeLabelIds",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "modifyMessageLabels"
          ]
        }
      },
      {
        "name": "markMessageAsRead",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark Gmail message as read (removes UNREAD label)",
        "returnType": "Promise<{ success: boolean; id?: string; labelIds: string[] }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "modifyMessageLabels",
            "markMessageAsUnread"
          ]
        }
      },
      {
        "name": "markMessageAsUnread",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Mark Gmail message as unread (adds UNREAD label)",
        "returnType": "Promise<{ success: boolean; id?: string; labelIds: string[] }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.0.0",
          "relatedTo": [
            "modifyMessageLabels",
            "markMessageAsRead"
          ]
        }
      },
      {
        "name": "sendMessage",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Send an email via Gmail. Supports plain-text or HTML body. When html is provided, sends a multipart/alternative message with both text/plain and text/html parts.",
        "returnType": "Promise<{ success: boolean; id: string; threadId?: string }>",
        "parameters": [
          {
            "name": "to",
            "type": "string"
          },
          {
            "name": "subject",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string"
          },
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "cc",
            "type": "string"
          },
          {
            "name": "bcc",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.1.0",
          "relatedTo": [
            "getMessages",
            "replyToMessage",
            "sendMessageWithAttachment"
          ]
        }
      },
      {
        "name": "sendMessageWithAttachment",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Send an email with file attachments via Gmail. Builds a MIME multipart/mixed message with a plain-text or HTML body and one or more base64-encoded attachments. When html is provided, the body is nested in multipart/alternative.",
        "returnType": "Promise<{ success: boolean; id: string; threadId?: string }>",
        "parameters": [
          {
            "name": "to",
            "type": "string"
          },
          {
            "name": "subject",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string"
          },
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "attachments",
            "type": "Array<{ filename: string; mimeType: string; base64Data: string }>"
          },
          {
            "name": "cc",
            "type": "string"
          },
          {
            "name": "bcc",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.1.0",
          "relatedTo": [
            "sendMessage",
            "generatePdf"
          ]
        },
        "docs": {
          "summary": "Send an email with file attachments via Gmail. Constructs a MIME multipart/mixed message with a text body and base64-encoded attachment parts. Useful for sending generated PDFs, reports, or other files.",
          "parameters": [
            {
              "name": "to",
              "type": "string",
              "description": "Recipient email address",
              "example": "\"client@example.com\""
            },
            {
              "name": "subject",
              "type": "string",
              "description": "Email subject line",
              "example": "\"Invoice #1001\""
            },
            {
              "name": "body",
              "type": "string",
              "description": "Plain-text email body (fallback for clients that don't support HTML)",
              "example": "\"Please find your invoice attached.\""
            },
            {
              "name": "html",
              "type": "string",
              "description": "Optional HTML email body. When provided, sends multipart/alternative with both plain text and HTML.",
              "optional": true
            },
            {
              "name": "attachments",
              "type": "Array<{ filename: string; mimeType: string; base64Data: string }>",
              "description": "Array of attachments. Each needs a filename, MIME type, and base64-encoded data.",
              "example": "[{ filename: \"invoice.pdf\", mimeType: \"application/pdf\", base64Data: \"JVBERi0xLjQK...\" }]"
            },
            {
              "name": "cc",
              "type": "string",
              "description": "CC recipients (optional)"
            },
            {
              "name": "bcc",
              "type": "string",
              "description": "BCC recipients (optional)"
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean; id: string; threadId?: string }>",
            "description": "Confirmation with the sent message ID and optional thread ID.",
            "example": "{ success: true, id: \"18e3a...\", threadId: \"18e3a...\" }"
          },
          "examples": [
            {
              "title": "Generate a PDF and email it",
              "code": "const pdf = await browser.generatePdf(\"<html><body><h1>Invoice</h1></body></html>\", { filename: \"invoice.pdf\" });\nawait gmail.sendMessageWithAttachment(\n  \"client@example.com\",\n  \"Your Invoice\",\n  \"Please find your invoice attached.\",\n  [{ filename: pdf.filename, mimeType: \"application/pdf\", base64Data: pdf.base64 }]\n);",
              "description": "Generate a PDF invoice and send it as an email attachment"
            }
          ],
          "seeAlso": [
            "gmail.sendMessage",
            "browser.generatePdf",
            "gmail.replyToMessage"
          ]
        }
      },
      {
        "name": "replyToMessage",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Reply to a Gmail message within its thread. Fetches the original message to set threading headers (In-Reply-To, References) and replies to the original sender. Supports HTML via multipart/alternative when html parameter is provided.",
        "returnType": "Promise<{ success: boolean; id: string; threadId?: string }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string"
          },
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "cc",
            "type": "string"
          },
          {
            "name": "bcc",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "auth"
          ],
          "version": "1.1.0",
          "relatedTo": [
            "sendMessage",
            "getMessage"
          ]
        }
      },
      {
        "name": "getMessageAttachment",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch a Gmail attachment by messageId + attachmentId. Returns standard base64 bytes (dataBase64).",
        "returnType": "Promise<{ messageId: string; attachmentId: string; size?: number; dataBase64: string }>",
        "parameters": [
          {
            "name": "messageId",
            "type": "string"
          },
          {
            "name": "attachmentId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessage",
            "parsePdf"
          ]
        }
      },
      {
        "name": "getThread",
        "requiredCredentials": [
          "google-oauth"
        ],
        "description": "Fetch all messages in a Gmail thread by threadId, in chronological order. Unlike getMessages, this preserves quoted reply chains so the full conversation context is available. Use this before replying to understand the complete thread history.",
        "returnType": "Promise<Array<{ id: string; subject?: string; from?: string; to?: string; date?: string; body: string; threadId?: string; labelIds?: string[] }>>",
        "parameters": [
          {
            "name": "threadId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "google",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessage",
            "getMessages",
            "replyToMessage"
          ]
        },
        "docs": {
          "summary": "Fetch all messages in a Gmail thread in chronological order. Preserves quoted reply content so the full conversation history is visible. Essential context before generating a reply.",
          "parameters": [
            {
              "name": "threadId",
              "type": "string",
              "description": "The Gmail thread ID (available as threadId on any GmailMessage object)",
              "example": "\"18f3a...\""
            }
          ],
          "returns": {
            "type": "Promise<GmailMessage[]>",
            "description": "Chronological array of all messages in the thread.",
            "example": "[{ id: \"18f...\", from: \"alice@example.com\", subject: \"Re: Project\", body: \"...\" }, ...]"
          },
          "examples": [
            {
              "title": "Get full thread before replying",
              "code": "const msgs = await gmail.getMessages(\"is:unread\", 5);\nfor (const msg of msgs) {\n  const thread = await gmail.getThread(msg.threadId);\n  const history = thread.map(m => `[${m.from}]: ${m.body}`).join(\"\\n\\n---\\n\\n\");\n  // pass history to agent for context\n}"
            }
          ],
          "seeAlso": [
            "getMessage",
            "getMessages",
            "replyToMessage"
          ]
        }
      }
    ]
  },
  {
    "slug": "github",
    "version": "1.0.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/github/1.0.2/28e7cf3b4f8e140b46777d6abf036da096bdab65cc5c545ae49f39506b3b915c.mjs",
    "digest": "28e7cf3b4f8e140b46777d6abf036da096bdab65cc5c545ae49f39506b3b915c",
    "contract_version": 1,
    "display_name": "GitHub",
    "description": "Work with GitHub issues, pull requests, branches, Actions and releases.",
    "category": "Developer",
    "auth": {
      "type": "token",
      "provider": "github",
      "label": "GitHub personal access token",
      "hint": "Create a classic token at github.com/settings/tokens — grant the repo scope (add workflow if you touch Actions)."
    },
    "marketplace": {
      "displayName": "GitHub",
      "category": "Developer",
      "tags": [
        "devops",
        "git"
      ],
      "useCases": [
        "Auto-triage and label incoming issues, then draft responses for review",
        "Review pull requests: pull the diff, summarize the risk, and request reviewers",
        "React to failed CI runs — pull job logs and re-run only the failed jobs",
        "Draft releases with a generated changelog from merged PRs",
        "Keep a branch current and open a pull request from a generated change"
      ],
      "permissionsSummary": "Read and write GitHub issues, pull requests, branches, files, Actions, and releases. Reads are non-mutating; writes and merges are mutating.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "c09bf5eeb7133e7b2edf2a314966653a5503c98f",
      "icon_url": "https://orgsdk.ai/plugins/icons/github.svg",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/github"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/github/1.0.2/28e7cf3b4f8e140b46777d6abf036da096bdab65cc5c545ae49f39506b3b915c.notices.txt"
    },
    "documentation": "# GitHub plugin\n\nWork with GitHub issues, pull requests, branches, Actions and releases.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `listWorkflowRuns` — List workflow runs for a repository. Optionally filter by actionId (filename or ID), branch, status ('queued', 'in_progress', 'completed').\n- `getWorkflowRun` — Get details of a specific workflow run by ID.\n- `triggerWorkflow` — Trigger a workflow_dispatch event. actionId can be the workflow filename (e.g. 'ci.yml') or numeric ID. ref is the branch/tag to run on.\n- `rerunWorkflowFailedJobs` — Re-run only the failed jobs of a workflow run. Returns immediately — poll getWorkflowRun to track progress.\n- `rerunWorkflow` — Re-run all jobs in a workflow run. Returns immediately — poll getWorkflowRun to track progress.\n- `cancelWorkflowRun` — Cancel an in-progress workflow run.\n- `listWorkflowJobs` — List jobs for a workflow run. Includes step-level detail so you can see which step failed. filter can be 'latest' (default) or 'all' (includes previous attempts).\n- `getWorkflowJobLogs` — Download logs for a specific job. Output is capped at 100KB. Use listWorkflowJobs first to get the jobId.\n- `listWorkflowRunArtifacts` — List artifacts produced by a workflow run.\n- `downloadWorkflowArtifact` — Get a temporary download URL for a workflow artifact (zip). The URL is short-lived. Use listWorkflowRunArtifacts first to get the artifactId.\n- `listBranches` — List branches for a GitHub repository.\n- `getBranch` — Get details of a specific branch including latest commit info.\n- `createBranch` — Create a new branch. fromRef defaults to the repo's default branch. Example: createBranch('owner', 'repo', 'feature/new', 'main').\n- `getFileContent` — Get the content of a file from a GitHub repository. Auto-decodes base64 to UTF-8. Errors on files >1MB. Use ref to specify a branch/tag/commit.\n- `createOrUpdateFile` — Create or update a file in a GitHub repository. Provide sha to update an existing file (get it from getFileContent). Content is auto-encoded to base64.\n- `listDirectoryContents` — List contents of a directory in a GitHub repository. Defaults to root. Use ref to specify a branch/tag/commit.\n- `listIssues` — List issues for a GitHub repository. Filters out pull requests. State: 'open', 'closed', 'all'. Labels: comma-separated string.\n- `getIssue` — Get a single GitHub issue by number.\n- `createIssue` — Create a new GitHub issue.\n- `updateIssue` — Update a GitHub issue. Only provided fields are changed. State: 'open' or 'closed'.\n- `addIssueComment` — Add a comment to a GitHub issue.\n- `listIssueComments` — List comments on a GitHub issue.\n- `addIssueLabels` — Add labels to a GitHub issue. Returns the full list of labels on the issue.\n- `listPullRequests` — List pull requests for a GitHub repository. State: 'open', 'closed', 'all'. head/base filter by branch name.\n- `getPullRequest` — Get a single pull request by number. Includes diff stats (additions, deletions, changedFiles).\n- `createPullRequest` — Create a new pull request. head = source branch, base = target branch (e.g. 'main').\n- `addPrComment` — Add a comment to a pull request.\n- `listPrComments` — List comments on a pull request.\n- `mergePullRequest` — Merge a pull request. mergeMethod: 'merge', 'squash', or 'rebase'. Defaults to 'merge'.\n- `requestPrReview` — Request reviewers for a pull request.\n- `listReleases` — List releases for a GitHub repository, ordered by creation date (newest first).\n- `getRelease` — Get a specific release by ID.\n- `createRelease` — Create a new GitHub release. tagName is required. targetCommitish defaults to the default branch.\n- `listRepos` — List GitHub repositories for a user or the authenticated user. Returns simplified repo objects.\n- `getRepo` — Get details of a specific GitHub repository.\n- `searchRepos` — Search GitHub repositories using GitHub search query syntax. Example: 'language:typescript stars:>100'.\n- `getRepoContributors` — List contributors for a GitHub repository.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "listWorkflowRuns",
        "requiredCredentials": [
          "github"
        ],
        "description": "List workflow runs for a repository. Optionally filter by actionId (filename or ID), branch, status ('queued', 'in_progress', 'completed').",
        "returnType": "Promise<Array<{ id: number; name: string | null; status: string | null; conclusion: string | null; headBranch: string | null; headSha: string; event: string; htmlUrl: string; runNumber: number; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "actionId",
            "type": "string | number",
            "optional": true
          },
          {
            "name": "branch",
            "type": "string",
            "optional": true
          },
          {
            "name": "status",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getWorkflowRun",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get details of a specific workflow run by ID.",
        "returnType": "Promise<{ id: number; name: string | null; status: string | null; conclusion: string | null; headBranch: string | null; headSha: string; event: string; htmlUrl: string; runNumber: number; createdAt: string; updatedAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "triggerWorkflow",
        "requiredCredentials": [
          "github"
        ],
        "description": "Trigger a workflow_dispatch event. actionId can be the workflow filename (e.g. 'ci.yml') or numeric ID. ref is the branch/tag to run on.",
        "returnType": "Promise<{ triggered: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "actionId",
            "type": "string | number"
          },
          {
            "name": "ref",
            "type": "string"
          },
          {
            "name": "inputs",
            "type": "Record<string, string>",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "rerunWorkflowFailedJobs",
        "requiredCredentials": [
          "github"
        ],
        "description": "Re-run only the failed jobs of a workflow run. Returns immediately — poll getWorkflowRun to track progress.",
        "returnType": "Promise<{ rerunned: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "rerunWorkflow",
        "requiredCredentials": [
          "github"
        ],
        "description": "Re-run all jobs in a workflow run. Returns immediately — poll getWorkflowRun to track progress.",
        "returnType": "Promise<{ rerunned: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "cancelWorkflowRun",
        "requiredCredentials": [
          "github"
        ],
        "description": "Cancel an in-progress workflow run.",
        "returnType": "Promise<{ cancelled: true }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listWorkflowJobs",
        "requiredCredentials": [
          "github"
        ],
        "description": "List jobs for a workflow run. Includes step-level detail so you can see which step failed. filter can be 'latest' (default) or 'all' (includes previous attempts).",
        "returnType": "Promise<Array<{ id: number; runId: number; name: string; status: string; conclusion: string | null; startedAt: string | null; completedAt: string | null; htmlUrl: string; runnerName: string | null; steps: Array<{ name: string; status: string; conclusion: string | null; number: number }> }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          },
          {
            "name": "filter",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getWorkflowJobLogs",
        "requiredCredentials": [
          "github"
        ],
        "description": "Download logs for a specific job. Output is capped at 100KB. Use listWorkflowJobs first to get the jobId.",
        "returnType": "Promise<{ logs: string; truncated: boolean }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "jobId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "listWorkflowRunArtifacts",
        "requiredCredentials": [
          "github"
        ],
        "description": "List artifacts produced by a workflow run.",
        "returnType": "Promise<Array<{ id: number; name: string; sizeInBytes: number; expired: boolean; expiresAt: string | null; createdAt: string | null; updatedAt: string | null }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "runId",
            "type": "number"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "downloadWorkflowArtifact",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a temporary download URL for a workflow artifact (zip). The URL is short-lived. Use listWorkflowRunArtifacts first to get the artifactId.",
        "returnType": "Promise<{ downloadUrl: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "artifactId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "listBranches",
        "requiredCredentials": [
          "github"
        ],
        "description": "List branches for a GitHub repository.",
        "returnType": "Promise<Array<{ name: string; sha: string; protected: boolean }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getBranch",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get details of a specific branch including latest commit info.",
        "returnType": "Promise<{ name: string; sha: string; protected: boolean; commitMessage: string; commitAuthor: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "branch",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createBranch",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new branch. fromRef defaults to the repo's default branch. Example: createBranch('owner', 'repo', 'feature/new', 'main').",
        "returnType": "Promise<{ ref: string; sha: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "branchName",
            "type": "string"
          },
          {
            "name": "fromRef",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "getFileContent",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get the content of a file from a GitHub repository. Auto-decodes base64 to UTF-8. Errors on files >1MB. Use ref to specify a branch/tag/commit.",
        "returnType": "Promise<{ name: string; path: string; sha: string; size: number; content: string; htmlUrl: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "path",
            "type": "string"
          },
          {
            "name": "ref",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createOrUpdateFile",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create or update a file in a GitHub repository. Provide sha to update an existing file (get it from getFileContent). Content is auto-encoded to base64.",
        "returnType": "Promise<{ path: string; sha: string; htmlUrl: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "path",
            "type": "string"
          },
          {
            "name": "content",
            "type": "string"
          },
          {
            "name": "message",
            "type": "string"
          },
          {
            "name": "branch",
            "type": "string",
            "optional": true
          },
          {
            "name": "sha",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listDirectoryContents",
        "requiredCredentials": [
          "github"
        ],
        "description": "List contents of a directory in a GitHub repository. Defaults to root. Use ref to specify a branch/tag/commit.",
        "returnType": "Promise<Array<{ name: string; path: string; type: 'file' | 'dir' | 'submodule' | 'symlink'; sha: string; size: number; htmlUrl: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "path",
            "type": "string",
            "optional": true
          },
          {
            "name": "ref",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "listIssues",
        "requiredCredentials": [
          "github"
        ],
        "description": "List issues for a GitHub repository. Filters out pull requests. State: 'open', 'closed', 'all'. Labels: comma-separated string.",
        "returnType": "Promise<Array<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "state",
            "type": "string",
            "optional": true
          },
          {
            "name": "labels",
            "type": "string",
            "optional": true
          },
          {
            "name": "assignee",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getIssue",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a single GitHub issue by number.",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createIssue",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new GitHub issue.",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "title",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "labels",
            "type": "string[]",
            "optional": true
          },
          {
            "name": "assignees",
            "type": "string[]",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "updateIssue",
        "requiredCredentials": [
          "github"
        ],
        "description": "Update a GitHub issue. Only provided fields are changed. State: 'open' or 'closed'.",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; labels: string[]; assignees: string[]; user: string | null; htmlUrl: string; commentsCount: number; createdAt: string; updatedAt: string; closedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "title",
            "type": "string",
            "optional": true
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "state",
            "type": "string",
            "optional": true
          },
          {
            "name": "labels",
            "type": "string[]",
            "optional": true
          },
          {
            "name": "assignees",
            "type": "string[]",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "addIssueComment",
        "requiredCredentials": [
          "github"
        ],
        "description": "Add a comment to a GitHub issue.",
        "returnType": "Promise<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "body",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listIssueComments",
        "requiredCredentials": [
          "github"
        ],
        "description": "List comments on a GitHub issue.",
        "returnType": "Promise<Array<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "addIssueLabels",
        "requiredCredentials": [
          "github"
        ],
        "description": "Add labels to a GitHub issue. Returns the full list of labels on the issue.",
        "returnType": "Promise<string[]>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "issueNumber",
            "type": "number"
          },
          {
            "name": "labels",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listPullRequests",
        "requiredCredentials": [
          "github"
        ],
        "description": "List pull requests for a GitHub repository. State: 'open', 'closed', 'all'. head/base filter by branch name.",
        "returnType": "Promise<Array<{ id: number; number: number; title: string; body: string | null; state: string; draft: boolean; merged: boolean; head: { ref: string; sha: string }; base: { ref: string; sha: string }; user: string | null; labels: string[]; htmlUrl: string; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "state",
            "type": "string",
            "optional": true
          },
          {
            "name": "head",
            "type": "string",
            "optional": true
          },
          {
            "name": "base",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getPullRequest",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a single pull request by number. Includes diff stats (additions, deletions, changedFiles).",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; draft: boolean; merged: boolean; head: { ref: string; sha: string }; base: { ref: string; sha: string }; user: string | null; labels: string[]; htmlUrl: string; additions: number; deletions: number; changedFiles: number; createdAt: string; updatedAt: string; mergedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createPullRequest",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new pull request. head = source branch, base = target branch (e.g. 'main').",
        "returnType": "Promise<{ id: number; number: number; title: string; body: string | null; state: string; draft: boolean; head: { ref: string; sha: string }; base: { ref: string; sha: string }; htmlUrl: string; createdAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "title",
            "type": "string"
          },
          {
            "name": "head",
            "type": "string"
          },
          {
            "name": "base",
            "type": "string"
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "draft",
            "type": "boolean",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "addPrComment",
        "requiredCredentials": [
          "github"
        ],
        "description": "Add a comment to a pull request.",
        "returnType": "Promise<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "body",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listPrComments",
        "requiredCredentials": [
          "github"
        ],
        "description": "List comments on a pull request.",
        "returnType": "Promise<Array<{ id: number; body: string; user: string | null; htmlUrl: string; createdAt: string; updatedAt: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "mergePullRequest",
        "requiredCredentials": [
          "github"
        ],
        "description": "Merge a pull request. mergeMethod: 'merge', 'squash', or 'rebase'. Defaults to 'merge'.",
        "returnType": "Promise<{ merged: boolean; message: string; sha: string }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "mergeMethod",
            "type": "string",
            "optional": true
          },
          {
            "name": "commitTitle",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "requestPrReview",
        "requiredCredentials": [
          "github"
        ],
        "description": "Request reviewers for a pull request.",
        "returnType": "Promise<{ requestedReviewers: string[] }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "pullNumber",
            "type": "number"
          },
          {
            "name": "reviewers",
            "type": "string[]"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listReleases",
        "requiredCredentials": [
          "github"
        ],
        "description": "List releases for a GitHub repository, ordered by creation date (newest first).",
        "returnType": "Promise<Array<{ id: number; tagName: string; name: string | null; body: string | null; draft: boolean; prerelease: boolean; htmlUrl: string; createdAt: string; publishedAt: string | null; assets: Array<{ name: string; size: number; downloadUrl: string }> }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getRelease",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get a specific release by ID.",
        "returnType": "Promise<{ id: number; tagName: string; name: string | null; body: string | null; draft: boolean; prerelease: boolean; htmlUrl: string; createdAt: string; publishedAt: string | null; assets: Array<{ name: string; size: number; downloadUrl: string }> }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "releaseId",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "createRelease",
        "requiredCredentials": [
          "github"
        ],
        "description": "Create a new GitHub release. tagName is required. targetCommitish defaults to the default branch.",
        "returnType": "Promise<{ id: number; tagName: string; name: string | null; body: string | null; draft: boolean; prerelease: boolean; htmlUrl: string; createdAt: string; publishedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "tagName",
            "type": "string"
          },
          {
            "name": "name",
            "type": "string",
            "optional": true
          },
          {
            "name": "body",
            "type": "string",
            "optional": true
          },
          {
            "name": "draft",
            "type": "boolean",
            "optional": true
          },
          {
            "name": "prerelease",
            "type": "boolean",
            "optional": true
          },
          {
            "name": "targetCommitish",
            "type": "string",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "auth",
            "io"
          ],
          "version": "1.0.0"
        }
      },
      {
        "name": "listRepos",
        "requiredCredentials": [
          "github"
        ],
        "description": "List GitHub repositories for a user or the authenticated user. Returns simplified repo objects.",
        "returnType": "Promise<Array<{ id: number; name: string; fullName: string; description: string | null; private: boolean; defaultBranch: string; language: string | null; stargazersCount: number; forksCount: number; openIssuesCount: number; htmlUrl: string; createdAt: string | null; updatedAt: string | null }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string",
            "optional": true
          },
          {
            "name": "type",
            "type": "string",
            "optional": true
          },
          {
            "name": "sort",
            "type": "string",
            "optional": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getRepo",
        "requiredCredentials": [
          "github"
        ],
        "description": "Get details of a specific GitHub repository.",
        "returnType": "Promise<{ id: number; name: string; fullName: string; description: string | null; private: boolean; defaultBranch: string; language: string | null; stargazersCount: number; forksCount: number; openIssuesCount: number; htmlUrl: string; createdAt: string | null; updatedAt: string | null }>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "searchRepos",
        "requiredCredentials": [
          "github"
        ],
        "description": "Search GitHub repositories using GitHub search query syntax. Example: 'language:typescript stars:>100'.",
        "returnType": "Promise<Array<{ id: number; name: string; fullName: string; description: string | null; private: boolean; defaultBranch: string; language: string | null; stargazersCount: number; forksCount: number; openIssuesCount: number; htmlUrl: string; createdAt: string | null; updatedAt: string | null }>>",
        "parameters": [
          {
            "name": "query",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "getRepoContributors",
        "requiredCredentials": [
          "github"
        ],
        "description": "List contributors for a GitHub repository.",
        "returnType": "Promise<Array<{ login: string; contributions: number; htmlUrl: string }>>",
        "parameters": [
          {
            "name": "owner",
            "type": "string"
          },
          {
            "name": "repo",
            "type": "string"
          },
          {
            "name": "maxResults",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "github",
          "tags": [
            "readonly",
            "auth",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      }
    ]
  },
  {
    "slug": "facebook",
    "version": "1.0.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/facebook/1.0.2/797e15e1507d476b6733a0682178e61d8562dc1640719fd631199d403aee9c56.mjs",
    "digest": "797e15e1507d476b6733a0682178e61d8562dc1640719fd631199d403aee9c56",
    "contract_version": 1,
    "display_name": "Facebook",
    "description": "Create and schedule Facebook Page posts, inspect insights and track engagement.",
    "category": "Social",
    "auth": {
      "type": "oauth2",
      "provider": "facebook-oauth",
      "scopes": [
        "pages_show_list",
        "pages_read_engagement",
        "pages_read_user_content",
        "pages_manage_posts",
        "pages_manage_engagement"
      ],
      "label": "Facebook OAuth",
      "hint": "Use your own provider OAuth application. Connect its client ID and secret in OrgSDK, then select that credential for this project."
    },
    "marketplace": {
      "displayName": "Facebook",
      "category": "Social",
      "tags": [
        "social",
        "posting",
        "analytics"
      ],
      "useCases": [
        "Schedule page posts with link attachments",
        "Report on post insights and reach",
        "List and manage pages you admin",
        "Track engagement across recent posts"
      ],
      "permissionsSummary": "Post, delete, and read insights for Facebook Pages via OAuth. Posting and deleting are public, mutating actions; deletes are irreversible.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "c9b0cd4dfaf816a497ccd6aef81849cff3c2b4c3",
      "icon_url": "https://orgsdk.ai/plugins/icons/facebook.svg",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/facebook"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/facebook/1.0.2/797e15e1507d476b6733a0682178e61d8562dc1640719fd631199d403aee9c56.notices.txt"
    },
    "documentation": "# Facebook plugin\n\nCreate and schedule Facebook Page posts, inspect insights and track engagement.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `getPage` — Get the Facebook business page info including name, link, and follower count.\n- `getPages` — List all Facebook business pages where the authenticated user has admin access.\n- `createPagePost` — Create a post on the Facebook business page. Supports link attachments and scheduled publishing.\n- `getPagePosts` — List recent posts on the Facebook business page with engagement metrics.\n- `getPostInsights` — Get detailed insights for a Facebook page post including impressions, reach, engagement, and click metrics.\n- `deletePagePost` — Delete a Facebook page post. This action is irreversible.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "getPage",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Get the Facebook business page info including name, link, and follower count.",
        "returnType": "Promise<{ id: string; name: string; link: string; fanCount?: number }>",
        "parameters": [],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve the Facebook business page info for the configured page ID. Returns name, link, and follower count.",
          "parameters": [],
          "returns": {
            "type": "Promise<FacebookPage>",
            "description": "The Facebook page details."
          }
        }
      },
      {
        "name": "getPages",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "List all Facebook business pages where the authenticated user has admin access.",
        "returnType": "Promise<Array<{ id: string; name: string; link: string; fanCount?: number }>>",
        "parameters": [],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "List all Facebook business pages where the authenticated user is an admin. Returns page IDs needed for posting.",
          "parameters": [],
          "returns": {
            "type": "Promise<FacebookPage[]>",
            "description": "Array of pages with ID, name, link, and follower count."
          }
        }
      },
      {
        "name": "createPagePost",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Create a post on the Facebook business page. Supports link attachments and scheduled publishing.",
        "returnType": "Promise<{ id: string; url: string }>",
        "parameters": [
          {
            "name": "message",
            "type": "string"
          },
          {
            "name": "linkUrl",
            "type": "string",
            "optional": true
          },
          {
            "name": "scheduledPublishTime",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "write",
            "social"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Publish a post on the Facebook business page. Supports link attachments and scheduled publishing. The post appears on the page timeline.",
          "parameters": [
            {
              "name": "message",
              "type": "string",
              "description": "The post message text.",
              "example": "\"Check out our new blog post!\""
            },
            {
              "name": "linkUrl",
              "type": "string",
              "description": "URL to attach as a link preview. Facebook will generate a preview card from Open Graph metadata.",
              "optional": true,
              "example": "\"https://quokly.com.au/blog/my-post\""
            },
            {
              "name": "scheduledPublishTime",
              "type": "number",
              "description": "Unix timestamp to schedule the post for future publication. Omit to publish immediately.",
              "optional": true,
              "example": "1715700000"
            }
          ],
          "returns": {
            "type": "Promise<{ id: string; url: string }>",
            "description": "The created post ID and Facebook URL."
          },
          "examples": [
            {
              "title": "Share a blog post link",
              "code": "const post = await facebook.createPagePost({\n  message: \"New on the blog: Why source transparency matters in AI research\",\n  linkUrl: \"https://quokly.com.au/blog/why-source-transparency\",\n});"
            },
            {
              "title": "Schedule a post for tomorrow",
              "code": "const tomorrow = Math.floor(Date.now() / 1000) + 86400;\nawait facebook.createPagePost({\n  message: \"Coming tomorrow: AI research for policy analysts\",\n  scheduledPublishTime: tomorrow,\n});"
            }
          ]
        }
      },
      {
        "name": "getPagePosts",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "List recent posts on the Facebook business page with engagement metrics.",
        "returnType": "Promise<Array<{ id: string; message?: string; createdTime: string; permalinkUrl: string; likes: number; comments: number; shares: number }>>",
        "parameters": [
          {
            "name": "limit",
            "type": "number",
            "optional": true
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "List recent posts on the Facebook business page with engagement metrics (likes, comments, shares).",
          "parameters": [
            {
              "name": "limit",
              "type": "number",
              "description": "Max number of posts to return. Default 10.",
              "optional": true,
              "defaultValue": "10"
            }
          ],
          "returns": {
            "type": "Promise<FacebookPagePost[]>",
            "description": "Array of page posts with engagement metrics."
          }
        }
      },
      {
        "name": "getPostInsights",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Get detailed insights for a Facebook page post including impressions, reach, engagement, and click metrics.",
        "returnType": "Promise<{ impressions: number; reach: number; likes: number; comments: number; shares: number; linkClicks: number; engagementRate: number }>",
        "parameters": [
          {
            "name": "postId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "readonly",
            "analytics"
          ],
          "readonly": true,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Retrieve detailed analytics for a Facebook page post. Returns impressions, reach, likes, comments, shares, link clicks, and engagement rate.",
          "parameters": [
            {
              "name": "postId",
              "type": "string",
              "description": "The Facebook post ID."
            }
          ],
          "returns": {
            "type": "Promise<FacebookPostInsights>",
            "description": "Detailed engagement and reach metrics."
          }
        }
      },
      {
        "name": "deletePagePost",
        "requiredCredentials": [
          "facebook-oauth"
        ],
        "description": "Delete a Facebook page post. This action is irreversible.",
        "returnType": "Promise<{ success: boolean }>",
        "parameters": [
          {
            "name": "postId",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "facebook",
          "tags": [
            "write",
            "destructive"
          ],
          "readonly": false,
          "version": "1.0.0"
        },
        "docs": {
          "summary": "Delete a Facebook page post. This action is irreversible.",
          "parameters": [
            {
              "name": "postId",
              "type": "string",
              "description": "The ID of the post to delete."
            }
          ],
          "returns": {
            "type": "Promise<{ success: boolean }>",
            "description": "Confirmation of deletion."
          }
        }
      }
    ]
  },
  {
    "slug": "data",
    "version": "1.0.2",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/data/1.0.2/da7c23b0ef61eeb767ef259057f8ccee4f22e4817614a04a3cd909dd0117ad4e.mjs",
    "digest": "da7c23b0ef61eeb767ef259057f8ccee4f22e4817614a04a3cd909dd0117ad4e",
    "contract_version": 1,
    "display_name": "Data Tools",
    "description": "Fetch HTTP data, transform arrays, convert currencies and extract text from PDFs.",
    "category": "Data & AI",
    "auth": {},
    "marketplace": {
      "displayName": "Data Tools",
      "category": "Data & AI",
      "tags": [
        "data",
        "utilities"
      ],
      "useCases": [
        "Fetch and transform JSON from external APIs",
        "Convert and reconcile currency amounts",
        "Parse PDFs and extract their text and metadata",
        "Chunk, deduplicate, and reshape datasets"
      ],
      "permissionsSummary": "Local data and file utilities plus outbound HTTP fetch. No stored credentials and no destructive external actions.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "6bbe9e39130ae2e4fe6f92c818b5a1d9c2591742",
      "icon_url": "",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/data"
      },
      "notices_url": "https://orgsdk.ai/plugins/packages/data/1.0.2/da7c23b0ef61eeb767ef259057f8ccee4f22e4817614a04a3cd909dd0117ad4e.notices.txt"
    },
    "documentation": "# Data Tools plugin\n\nFetch HTTP data, transform arrays, convert currencies and extract text from PDFs.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `convertCurrency` — Convert currency amount from one currency to another. IMPORTANT: Returns OBJECT with {convertedAmount: number, originalAmount: number, rate: number}. Use result.convertedAmount to get the numeric converted value. Example: const result = await convertCurrency(100, 'USD', 'AUD'); const converted = result.convertedAmount; Available rates: USD_TO_AUD=1.53, EUR_TO_AUD=1.67, GBP_TO_AUD=1.92\n- `fetchData` — Fetch JSON data from a URL\n- `sumArray` — Sum all numbers in an array\n- `chunkArray` — Split array into chunks of specified size\n- `uniqueBy` — Remove duplicates from array by key\n- `groupBy` — Group array items by key\n- `processData` — Process data array\n- `transformData` — Transform data array with various operations\n- `saveResult` — Save a result to storage\n- `validateData` — Validate data against rules\n- `parsePdf` — Extract text content and metadata from a PDF. Accepts standard base64 string (e.g. from getMessageAttachment().dataBase64). Returns full text, page count, and optional metadata.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "convertCurrency",
        "description": "Convert currency amount from one currency to another. IMPORTANT: Returns OBJECT with {convertedAmount: number, originalAmount: number, rate: number}. Use result.convertedAmount to get the numeric converted value. Example: const result = await convertCurrency(100, 'USD', 'AUD'); const converted = result.convertedAmount; Available rates: USD_TO_AUD=1.53, EUR_TO_AUD=1.67, GBP_TO_AUD=1.92",
        "returnType": "Promise<{ originalAmount: number; fromCurrency: string; convertedAmount: number; toCurrency: string; rate: number }>",
        "parameters": [
          {
            "name": "amount",
            "type": "number"
          },
          {
            "name": "fromCurrency",
            "type": "string"
          },
          {
            "name": "toCurrency",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "fetchData",
        "description": "Fetch JSON data from a URL",
        "returnType": "Promise<any>",
        "parameters": [
          {
            "name": "url",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "network",
            "discovery"
          ],
          "readonly": true,
          "discovery": true,
          "version": "1.0.0"
        }
      },
      {
        "name": "sumArray",
        "description": "Sum all numbers in an array",
        "returnType": "number",
        "parameters": [
          {
            "name": "numbers",
            "type": "number[]"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "chunkArray",
            "groupBy"
          ]
        }
      },
      {
        "name": "chunkArray",
        "description": "Split array into chunks of specified size",
        "returnType": "T[][]",
        "parameters": [
          {
            "name": "array",
            "type": "T[]"
          },
          {
            "name": "size",
            "type": "number"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "sumArray",
            "uniqueBy"
          ]
        }
      },
      {
        "name": "uniqueBy",
        "description": "Remove duplicates from array by key",
        "returnType": "T[]",
        "parameters": [
          {
            "name": "array",
            "type": "T[]"
          },
          {
            "name": "key",
            "type": "keyof T"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "groupBy",
            "chunkArray"
          ]
        }
      },
      {
        "name": "groupBy",
        "description": "Group array items by key",
        "returnType": "Record<string, T[]>",
        "parameters": [
          {
            "name": "array",
            "type": "T[]"
          },
          {
            "name": "key",
            "type": "keyof T"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "uniqueBy",
            "sumArray"
          ]
        }
      },
      {
        "name": "processData",
        "description": "Process data array",
        "returnType": "any[]",
        "parameters": [
          {
            "name": "data",
            "type": "any[]"
          },
          {
            "name": "processor",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "transformData"
          ]
        }
      },
      {
        "name": "transformData",
        "description": "Transform data array with various operations",
        "returnType": "any",
        "parameters": [
          {
            "name": "data",
            "type": "any[]"
          },
          {
            "name": "transformer",
            "type": "string"
          },
          {
            "name": "key",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly",
            "data"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "processData"
          ]
        }
      },
      {
        "name": "saveResult",
        "description": "Save a result to storage",
        "returnType": "{ key: string; value: any; saved: boolean }",
        "parameters": [
          {
            "name": "key",
            "type": "string"
          },
          {
            "name": "value",
            "type": "any"
          },
          {
            "name": "storage",
            "type": "Record<string, any>"
          }
        ]
      },
      {
        "name": "validateData",
        "description": "Validate data against rules",
        "returnType": "{ valid: boolean; errors: string[] }",
        "parameters": [
          {
            "name": "data",
            "type": "any"
          },
          {
            "name": "rules",
            "type": "Record<string, any>"
          }
        ]
      },
      {
        "name": "parsePdf",
        "description": "Extract text content and metadata from a PDF. Accepts standard base64 string (e.g. from getMessageAttachment().dataBase64). Returns full text, page count, and optional metadata.",
        "returnType": "Promise<{ text: string; totalPages: number; metadata?: { title?: string; author?: string; subject?: string; creator?: string; producer?: string } }>",
        "parameters": [
          {
            "name": "base64Data",
            "type": "string"
          }
        ],
        "metadata": {
          "category": "data",
          "tags": [
            "readonly"
          ],
          "readonly": true,
          "version": "1.0.0",
          "relatedTo": [
            "getMessageAttachment"
          ]
        },
        "docs": {
          "summary": "Extract text content and metadata from a PDF document. Accepts a standard base64-encoded string — the same format returned by getMessageAttachment().dataBase64.",
          "parameters": [
            {
              "name": "base64Data",
              "type": "string",
              "description": "Standard base64-encoded PDF bytes. Pass the dataBase64 field from getMessageAttachment() directly.",
              "example": "\"JVBERi0xLjQK...\""
            }
          ],
          "returns": {
            "type": "Promise<{ text: string; totalPages: number; metadata?: Record<string, string> }>",
            "description": "Extracted text (all pages merged), total page count, and optional PDF metadata (title, author, subject, creator, producer).",
            "example": "{ text: \"Invoice #1234\\nDate: 2026-01-15\\nAmount: $500.00\", totalPages: 2, metadata: { title: \"Invoice\", author: \"Acme Corp\" } }"
          },
          "examples": [
            {
              "title": "Extract text from a Gmail PDF attachment",
              "code": "const msgs = await gmail.getMessages(\"has:attachment\", 5);\nconst msg = msgs[0];\nconst pdfRef = msg.attachments?.find(a => a.filename.endsWith(\".pdf\"));\nif (pdfRef) {\n  const attachment = await gmail.getMessageAttachment(msg.id, pdfRef.attachmentId);\n  const pdf = await data.parsePdf(attachment.dataBase64);\n  pdf.text;\n}",
              "description": "Full chain: find email with attachment, download it, extract text"
            }
          ],
          "caveats": [
            "Scanned/image-only PDFs will return empty or minimal text — OCR is not supported",
            "Very large PDFs may be slow; consider processing only the attachments you need"
          ],
          "seeAlso": [
            "getMessageAttachment",
            "getMessage"
          ]
        }
      }
    ]
  },
  {
    "slug": "browser",
    "version": "1.1.1",
    "distribution": "external",
    "package_url": "https://orgsdk.ai/plugins/packages/browser/1.1.1/e2fc827a33bf16e602e3a1f9a4c05587278720df6a9640cfff385b42d4efe229.mjs",
    "digest": "e2fc827a33bf16e602e3a1f9a4c05587278720df6a9640cfff385b42d4efe229",
    "contract_version": 1,
    "display_name": "Browser",
    "description": "Render HTML reports, invoices and documents to PDF with headless Chromium.",
    "category": "Documents & Output",
    "auth": {},
    "marketplace": {
      "displayName": "Browser",
      "category": "Documents & Output",
      "tags": [
        "automation",
        "pdf",
        "documents"
      ],
      "useCases": [
        "Generate a PDF report from project output and email it",
        "Render invoices, receipts, or summaries to PDF",
        "Produce a downloadable artifact from a workflow"
      ],
      "permissionsSummary": "Local headless browser rendering only. JavaScript is disabled and all external/resource requests are blocked. No credentials, no network egress, no destructive actions.",
      "license": "OrgSDK Source-Available License 1.0",
      "license_url": "https://orgsdk.ai/plugins/license.txt",
      "source_commit": "fbdab76e3e70fd5b3c08b2edd4d8c00752d0978a",
      "icon_url": "",
      "links": {
        "homepage": "https://orgsdk.ai/plugins/browser"
      },
      "runtime_requirements": [
        "Chromium for Playwright 1.60.0 must be installed on the runner. The optional Chromium executor Dockerfile supplies the matching engine."
      ],
      "runtime_dockerfile": "https://orgsdk.ai/plugins/browser/Dockerfile",
      "notices_url": "https://orgsdk.ai/plugins/packages/browser/1.1.1/e2fc827a33bf16e602e3a1f9a4c05587278720df6a9640cfff385b42d4efe229.notices.txt"
    },
    "documentation": "# Browser plugin\n\nRender HTML reports, invoices and documents to PDF with headless Chromium.\n\n## Install\nOpen a project in OrgSDK, select Plugins, find this plugin and choose Enable plugin. The executor downloads the versioned package and verifies its SHA256 checksum.\nConnect a credential for this project if the plugin requires one. OAuth connections require your own provider application; there is no hosted or first-party OAuth relay.\n\n## Calls and actions\nCall these tools through the plugin namespace in a saved action or a direct code run. Pass named argument objects. The release metadata contains each tool’s parameters and return type.\n\n- `generatePdf` — Generate a PDF from HTML content using headless Chromium. Returns base64-encoded PDF bytes.\n\n## Chromium runner prerequisite\nThe package includes its Playwright JavaScript dependency, but the browser binary belongs in the runner environment. Build the optional executor image from an OrgSDK checkout:\n```sh\ncurl -fsSL https://orgsdk.ai/plugins/browser/Dockerfile -o /tmp/orgsdk-chromium.Dockerfile\ndocker build -t orgsdk-job:chromium -f /tmp/orgsdk-chromium.Dockerfile runtime\nmake EXECUTOR_JOB_IMAGE=orgsdk-job:chromium executor\n```\nFor independently managed executors, select this image through EXECUTOR_IMAGE. It supplies Chromium for Playwright 1.60.0 at PLAYWRIGHT_BROWSERS_PATH=/ms-playwright. The same JavaScript bundle works in a host-mode runner with matching Chromium installed at its configured PLAYWRIGHT_BROWSERS_PATH.\n\n## License\nFirst-party plugin code is distributed under the OrgSDK Source-Available License 1.0. Dependency license notices accompany and are embedded in the package. The GitHub source repository remains private.\n",
    "configuration_schema": {
      "type": "object",
      "additionalProperties": true
    },
    "tools": [
      {
        "name": "generatePdf",
        "description": "Generate a PDF from HTML content using headless Chromium. Returns base64-encoded PDF bytes.",
        "returnType": "Promise<{ base64: string; sizeBytes: number; filename: string }>",
        "parameters": [
          {
            "name": "html",
            "type": "string"
          },
          {
            "name": "options",
            "type": "{ format?: \"A4\" | \"Letter\"; landscape?: boolean; margin?: { top?: string; right?: string; bottom?: string; left?: string }; filename?: string }"
          }
        ],
        "metadata": {
          "category": "browser",
          "tags": [
            "io"
          ],
          "version": "1.1.1",
          "relatedTo": [
            "parsePdf",
            "sendMessageWithAttachment",
            "upsertArtifact"
          ]
        },
        "docs": {
          "summary": "Generate a PDF document from HTML content. Uses headless Chromium via Playwright to render the HTML and produce a PDF. Returns base64-encoded bytes suitable for storing as an artifact or attaching to an email. JavaScript is disabled and all external/resource requests are blocked — only inline styles and data:/blob: images load.",
          "parameters": [
            {
              "name": "html",
              "type": "string",
              "description": "Full HTML content to render as a PDF. Inline CSS and tables are supported; images must be embedded as data URIs.",
              "example": "\"<html><body><h1>Invoice #1001</h1><table>...</table></body></html>\""
            },
            {
              "name": "options",
              "type": "object",
              "description": "Optional PDF generation settings.",
              "example": "{ format: \"A4\", landscape: false, filename: \"invoice-1001.pdf\" }"
            }
          ],
          "returns": {
            "type": "Promise<{ base64: string; sizeBytes: number; filename: string }>",
            "description": "Base64-encoded PDF bytes, file size in bytes, and the filename (auto-generated if not provided).",
            "example": "{ base64: \"JVBERi0xLjQK...\", sizeBytes: 45230, filename: \"invoice-1001.pdf\" }"
          },
          "examples": [
            {
              "title": "Generate an invoice PDF and email it",
              "code": "const html = `<html>\n<head><style>table { width: 100%; border-collapse: collapse; } td, th { border: 1px solid #ddd; padding: 8px; }</style></head>\n<body>\n  <h1>Invoice #1001</h1>\n  <table><tr><th>Item</th><th>Amount</th></tr><tr><td>Consulting</td><td>$500</td></tr></table>\n</body></html>`;\n\nconst pdf = await generatePdf({ html, options: { format: \"A4\", filename: \"invoice-1001.pdf\" } });\nawait upsertArtifact(\"report\", { base64: pdf.base64, filename: pdf.filename }, { name: \"invoice-1001\", tags: [\"invoice\"] });\nawait sendMessageWithAttachment(\"client@example.com\", \"Invoice #1001\", \"Please find your invoice attached.\", [{ filename: pdf.filename, mimeType: \"application/pdf\", base64Data: pdf.base64 }]);",
              "description": "Generate an invoice PDF from HTML, store it as an artifact, and email it"
            }
          ],
          "caveats": [
            "JavaScript is disabled — <script> tags and inline event handlers do not execute.",
            "All external resource requests (http, https, file, fonts, remote images) are blocked. Embed images and fonts as data URIs.",
            "Renders are serialized (one at a time per process) and capped at a 30s render timeout."
          ],
          "seeAlso": [
            "parsePdf",
            "sendMessageWithAttachment",
            "upsertArtifact"
          ]
        }
      }
    ]
  }
]
