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": "summarize-skill",
"type": "skill",
"version": "0.1.0",
"name": "Summarize",
"author": "Undra",
"description": "An AI skill that condenses long documents into a tight summary.",
"license": "CC0-1.0",
"tags": ["ai", "writing"],
"screenshots": [],
"verified": true,
"downloads": 0,
"manifestUrl": "https://community.undra.com/resources/summarize-skill/0.1.0/manifest.toml",
"payloadBaseUrl": "https://community.undra.com/resources/summarize-skill/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 | "skill" | "extension" | "font" | "agent" |
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. { skill: { entry: "skill.md" } }). Embedded so installers don't need to re-fetch + parse the TOML manifest. Schema matches the [payload.<type>] block below. |
resources[].media |
object | null | no | Resolved from the optional [media] table: { "heroUrl": URL|null, "screenshotUrls": URL[], "videoUrl": URL|null }. All URLs absolute under payloadBaseUrl. null when the manifest declares no media — renderers fall back to type-specific auto-previews. |
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 = "summarize-skill" # string, required, kebab-case
type = "skill" # enum, required
version = "0.1.0" # semver, required
name = "Summarize" # string, required
author = "Undra" # string, required
authorUrl = "https://www.undra.com" # URL, optional
description = "Condense long docs..." # string, required, ≤ 200 chars
longDescription = """
Markdown allowed here.
""" # string, optional
license = "CC0-1.0" # SPDX, required
tags = ["ai", "writing"] # 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
Media (optional) — [media]
Presentation media for the storefront. Entirely optional — resources without
media get an auto-generated preview (font specimen or a deterministic
thumbnail). All paths are relative to the version dir and must
exist on disk (validate-manifests.js enforces this).
[media]
hero = "hero.png" # string, optional — 16:9 recommended; card + detail hero
screenshots = ["shot-1.png", "shot-2.png"] # string[], optional — detail-page gallery
video = "demo.webm" # string, optional — detail-page muted looping autoplay
herois shown as the card's cover image (object-fit cover, 16:9 region) and as the big visual atop the detail page. Aim for ≥ 1280×720.screenshotsrender as a click-to-open grid on the detail page.videoshould be short, silent, and small (webm/mp4) — it autoplays muted in a loop on the detail page.
Per-type payload
Exactly one of the following tables MUST be present, matching type.
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.
Agent — [payload.agent]
[payload.agent]
entry = "AGENT.md" # agent definition file (frontmatter + system prompt, Anthropic-skills-style)
model = "claude-opus-4-8" # optional — pinned model id the agent runs on
icon = "icon.png" # optional — icon file shipped alongside the manifest
tools = ["undra_fetch_url", "undra_create_note"]
requiredCapabilities = ["workspace:read"]
optionalCapabilities = ["network:fetch"]
An agent is a downloadable AI assistant with a system prompt, a tool allowlist, and permissions, all declared in a single markdown file. The [payload.agent] table mirrors the skill table: entry names the markdown file that holds the agent definition (conventionally AGENT.md), whose frontmatter declares the name, description, tools, and permissions. Two agent-specific fields are allowed: model (an optional pinned model id the agent should run on) and icon (an optional icon file relative to the manifest). Both are optional; when model is absent the app picks its default model.
Installer behavior: downloads files into ~/.undra/agents/<id>/, registers with ai-runtime as a saved agent. Install dialog shows requiredCapabilities for user consent.
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