# Resource Schema Defines two file shapes the catalog produces: 1. **`index.json`** — single lightweight catalog at the repo root, fetched by the in-app Community panel. 2. **`manifest.toml`** — one per `resources///` folder, full detail for a single resource. The index is a cache of the manifests. The manifest is the source of truth; the index is regenerated from manifests by `scripts/build-index.js`. --- ## `index.json` ```json { "schemaVersion": 1, "generatedAt": "2026-05-20T00:00:00Z", "resources": [ { "id": "welcome-note", "type": "template", "version": "0.1.0", "name": "Welcome to Undra", "author": "Undra", "description": "A first-run welcome note that introduces the workspace.", "license": "CC0-1.0", "tags": ["onboarding", "note"], "screenshots": [], "verified": true, "downloads": 0, "manifestUrl": "https://community.undra.com/resources/welcome-note/0.1.0/manifest.toml", "payloadBaseUrl": "https://community.undra.com/resources/welcome-note/0.1.0/" } ] } ``` ### Fields | Field | Type | Required | Notes | |---|---|---|---| | `schemaVersion` | int | yes | Bump when the index shape changes incompatibly. | | `generatedAt` | ISO-8601 string | yes | Set by `build-index.js` at generation time. | | `resources` | array | yes | One entry per `(id, version)` pair — multiple versions of the same id may coexist. | | `resources[].id` | string | yes | Kebab-case, globally unique. | | `resources[].type` | enum | yes | `"template" \| "skill" \| "extension" \| "font" \| "prompt"` | | `resources[].version` | semver string | yes | `MAJOR.MINOR.PATCH`. | | `resources[].name` | string | yes | Display name. | | `resources[].author` | string | yes | Display author. | | `resources[].description` | string | yes | One-line description (≤ 200 chars). | | `resources[].license` | SPDX string | yes | E.g. `"MIT"`, `"CC0-1.0"`, `"OFL-1.1"`. | | `resources[].tags` | string[] | no | Free-form, lowercase. | | `resources[].screenshots` | string[] | no | URLs (absolute or `payloadBaseUrl`-relative). | | `resources[].verified` | bool | no | True for curator-reviewed entries in this repo. | | `resources[].downloads` | int | no | Static for v1; placeholder for future analytics. | | `resources[].manifestUrl` | URL | yes | Absolute URL to the full manifest. | | `resources[].payloadBaseUrl` | URL | yes | Absolute URL prefix for resolving payload files. Trailing slash required. | | `resources[].payload` | object | yes | Full per-type payload table from the manifest (e.g. `{ template: { files: [...] } }`). Embedded so installers don't need to re-fetch + parse the TOML manifest. Schema matches the `[payload.]` block below. | --- ## `manifest.toml` Every `resources///manifest.toml` follows this shape. Shared fields at the top, then **exactly one** `[payload.]` table. ### Shared fields ```toml id = "welcome-note" # string, required, kebab-case type = "template" # enum, required version = "0.1.0" # semver, required name = "Welcome to Undra" # string, required author = "Undra" # string, required authorUrl = "https://www.undra.com" # URL, optional description = "A first-run welcome..." # string, required, ≤ 200 chars longDescription = """ Markdown allowed here. """ # string, optional license = "CC0-1.0" # SPDX, required tags = ["onboarding", "note"] # string[], optional homepage = "https://..." # URL, optional screenshots = ["screenshot.png"] # string[], optional, relative to manifest verified = true # bool, optional, default false minAppVersion = "0.1.0" # semver, optional ``` ### Per-type payload Exactly **one** of the following tables MUST be present, matching `type`. #### Template — `[payload.template]` ```toml [payload.template] itemType = "note" # "note" | "plan" | "canvas" | "dashboard" files = ["welcome.md"] # files, relative to payloadBaseUrl defaultTargetFolder = "" # "" = workspace root, else path ``` Installer behavior: copies `files` into `/.undra/templates//` and registers them with the item-create flow for `itemType`. #### Font — `[payload.font]` ```toml [payload.font] family = "Inter" category = "sans-serif" # "sans-serif" | "serif" | "monospace" | "display" variableFont = false [[payload.font.faces]] weight = 400 style = "normal" # "normal" | "italic" file = "Inter-Regular.woff2" [[payload.font.faces]] weight = 700 style = "normal" file = "Inter-Bold.woff2" ``` Installer behavior: downloads each `faces[].file` into `~/.undra/fonts//`, registers the family with the renderer, exposes it in the font picker. #### Skill — `[payload.skill]` ```toml [payload.skill] entry = "skill.md" # skill definition file (Anthropic-skills-style markdown frontmatter) tools = ["workspace_search", "workspace_read"] requiredCapabilities = ["workspace:read"] optionalCapabilities = ["network:fetch"] ``` Installer behavior: downloads files into `~/.undra/skills//`, registers with `ai-runtime`. **Install dialog shows `requiredCapabilities` for user consent.** #### Prompt — `[payload.prompt]` ```toml [payload.prompt] entry = "prompt.md" # markdown file containing the prompt body model = "claude-sonnet-4" # optional — suggested model hint ``` Installer behavior: copies `entry` into `/.undra/prompts///`. Prompts are deliberately **tool-agnostic** — they're just markdown text. Authors are encouraged to write prompts that work across Claude, Codex, Cursor, etc., not Undra-specific syntax. #### Extension — `[payload.extension]` ```toml [payload.extension] entry = "extension.js" # script-worker entry point contributes = ["commands", "panels"] # "commands" | "panels" | "item-types" | "views" | "ai-tools" requiredCapabilities = ["workspace:read"] optionalCapabilities = ["network:fetch", "ai:tools"] ``` Installer behavior: downloads files into `~/.undra/extensions//`, loads into the existing script-worker sandbox, registers contributions with the extension host. **Install dialog shows `requiredCapabilities` for user consent.** --- ## Capability vocabulary (v1) Used by `requiredCapabilities` / `optionalCapabilities` on skills and extensions. The undra-app extension host gates each one. | Capability | Grants | |---|---| | `workspace:read` | Read items, folders, tags from the current workspace. | | `workspace:write` | Create, update, delete items. | | `network:fetch` | Make outbound HTTP requests. | | `ai:tools` | Call tools through the AI runtime. | | `ai:skills` | Invoke other registered skills. | | `storage:local` | Persist per-extension key/value data. | | `notifications` | Emit user-visible notifications. | This list will grow. Adding a capability is a coordinated change between the extension host (`apps/desktop/src/extensions/runtime/capabilities.ts`) and this document. --- ## Versioning rules - New version of an existing resource = new sibling folder under `resources///`. Old version is never modified or moved. - Multiple versions appear as separate entries in `index.json`; the app picks the latest compatible (`minAppVersion`) by default. - Removing a version = removing the folder. The app will treat any locally installed copy as orphaned but functional. ## Validation `scripts/validate-manifests.js` (to be implemented) MUST verify: - Folder name `/` matches the manifest's `id` and `version` - All required shared fields present - Exactly one `[payload.]` table present, matching `type` - All referenced files (`files`, `faces[].file`, `entry`, etc.) exist on disk - `license` is a known SPDX identifier - `id` is kebab-case, globally unique across the repo