Resource Schema
Defines two file shapes the catalog produces:
index.json— single lightweight catalog at the repo root, fetched by the in-app Community panel.manifest.toml— one perresources/<id>/<version>/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
{
"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.<type>] block below. |
manifest.toml
Every resources/<id>/<version>/manifest.toml follows this shape. Shared fields at the top, then exactly one [payload.<type>] table.
Shared fields
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]
[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 <workspace>/.undra/templates/<id>/ and registers them with the item-create flow for itemType.
Font — [payload.font]
[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/<id>/, registers the family with the renderer, exposes it in the font picker.
Skill — [payload.skill]
[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/<id>/, registers with ai-runtime. Install dialog shows requiredCapabilities for user consent.
Prompt — [payload.prompt]
[payload.prompt]
entry = "prompt.md" # markdown file containing the prompt body
model = "claude-sonnet-4" # optional — suggested model hint
Installer behavior: copies entry into <workspace>/.undra/prompts/<id>/<version>/. 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]
[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/<id>/, 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/<id>/<new-version>/. 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
<id>/<version>matches the manifest'sidandversion - All required shared fields present
- Exactly one
[payload.<type>]table present, matchingtype - All referenced files (
files,faces[].file,entry, etc.) exist on disk licenseis a known SPDX identifieridis kebab-case, globally unique across the repo