# Commands & AI context Two small contribution types: **commands** that show up in the command palette, and **AI context providers** that teach Undra's agents about your extension's data. Both follow the same loadable-extension shape you already know from the [overview](/docs/extensions/): one `extension.js`, a `manifest` with the right capability, and an `activate(ctx)` that registers contributions. If you have not built one yet, read [the ctx API](/docs/extensions/ctx-api/) first. Neither of these contributions needs an item type, so they are the lightest things you can ship. :::note Two separate capabilities Commands need `commands.registry`. Context providers need `contextProviders.registry`. Declare whichever you use in `manifest.capabilities`. They are independent, declare one or both. ::: ## Commands {#commands} A command is a named action that appears in Undra's command palette. The user opens the palette, types your command's title, hits enter, and your `handler` runs. That is the whole model. ### The shape Register commands with `ctx.registerCommands([...])`. Each command is a `CommandContribution`: ```ts type CommandContribution = { id: string // unique, namespace it with your publisher title: string // what the user sees in the palette category: string // groups related commands in the palette handler: (ctx: unknown) => void | Promise // runs when invoked; may be async } ``` Field notes: - **`id`** is your stable handle. Namespace it (for example `community.example.todo.archiveDone`) so it never collides with another extension's command. - **`title`** is the searchable label. Write it as an action: "Archive completed todos", not "Todos". - **`category`** is a grouping label shown alongside the title (for example "Todo"). Keep it short and reuse the same category across your commands. - **`handler`** does the work. Return a promise if it is async. The argument is the host-provided invocation context, treat it as opaque, your real entry point to the workspace is the `ctx` you captured in `activate` (closures work, see below). ### A runnable example This extension adds one palette command that creates a dated note. No item type, no editor, just an action. ```javascript file="extension.js" export const manifest = { id: 'community.example.dailynote', version: '0.1.0', displayName: 'Daily Note', publisher: 'example', capabilities: ['commands.registry'], } export function activate(ctx) { ctx.registerCommands([ { id: 'community.example.dailynote.create', title: 'Create today\'s daily note', category: 'Daily Note', handler: async function () { const today = new Date().toISOString().slice(0, 10) // YYYY-MM-DD await ctx.workspace.create({ type: 'note', title: 'Daily ' + today, content: '# ' + today + '\n\n', }) }, }, ]) return function deactivate() {} } ``` Drop it at `/.undra/extensions/dailynote/extension.js`, restart Undra, open the command palette, and type "daily". Your command is there. Selecting it creates a fresh dated note. :::tip The handler closes over `activate`'s `ctx` The handler reaches the workspace through the `ctx` you captured in `activate` (the example calls `ctx.workspace.create`), not through the `handler`'s own argument. Capture anything you need (the workspace, the query API, your own state) in the closure. See [the ctx API](/docs/extensions/ctx-api/) for `workspace.create`, `workspace.update`, and the read-only `query` surface. ::: ## AI context providers {#context-providers} A context provider is how your extension talks to Undra's AI agents. When a chat or agent run is being prepared, Undra builds a prompt under a fixed token budget. Your provider gets called and can contribute two things into that budget: **sticky facts** (short, high-value lines the model should always know) and **selected prompt lines** (larger blocks, for example the body of a selected item). This is how you teach an agent about a custom item type or a piece of state it would otherwise have no idea exists. Think of it as briefing the AI. If your extension stores data the model cannot see (a JSON body, an external sync, a computed status), a context provider is where you hand the model a one-line summary so its answers are grounded instead of guessed. ### The shape Register a provider with `ctx.registry.registerContextProvider(manifest.id, contrib)`. The contribution is a `ContextProviderContribution`: ```ts type ContextProviderContribution = { id: string order?: number // lower runs first; default 100 critical?: boolean // if true, a failure surfaces instead of being silently isolated collect?: (ctx: unknown) => Promise collectSync?: (ctx: unknown) => ContextProviderCollectResult | null | undefined } ``` You provide `collect`, `collectSync`, or both: - **`collectSync`** runs on the fast prompt-preview path. Use it for cheap, immediate sticky facts (no awaiting, no I/O). If the host is just previewing what the prompt will look like, this is what it calls. - **`collect`** is the async path. Use it for heavier work: reading a selected item's body, querying the workspace, hitting a cache. Return `null` (or `undefined`) when you have nothing to add, the host skips you cleanly. - **`order`** controls merge order across all providers (yours and Undra's own). Lower numbers run first. Leave it at the default `100` unless you have a reason to sort earlier or later. - **`critical`** defaults to `false`, which means if your provider throws, Undra isolates the failure and drops your slice rather than breaking the whole prompt. Set it to `true` only when missing your context is worse than a visible error. What you return is a `ContextProviderCollectResult`: ```ts type ContextProviderCollectResult = { stickyFacts?: { tier: 'critical' | 'important' | 'supplemental'; text: string }[] selectedPromptLines?: string[] } ``` - **`stickyFacts`** are short lines the model keeps in view. Each has a `tier`. When the budget is tight, `critical` survives, `important` is next, and `supplemental` is dropped first. Spend `critical` sparingly, it is the most expensive real estate in the prompt. - **`selectedPromptLines`** are full prompt lines (for example the body of the item the user has selected). They are merged in registry `order`. Use them for bulk content, not for the always-on summary. :::warn Sticky facts cost budget Every sticky fact competes for a fixed prompt budget with everything else (other extensions, Undra's own context, the conversation). Keep each line tight and factual. One good `critical` fact beats five vague `supplemental` ones. Do not dump whole records here, that is what `selectedPromptLines` is for. ::: ### A runnable example This pairs with a hypothetical "todo" item type (see [item types](/docs/extensions/item-types/)) and teaches the AI how many todos are open, so an agent asked "what is on my plate" answers from real data instead of hallucinating. ```javascript file="extension.js" export const manifest = { id: 'community.example.todocontext', version: '0.1.0', displayName: 'Todo AI Context', publisher: 'example', capabilities: ['contextProviders.registry'], } export function activate(ctx) { ctx.registry.registerContextProvider(manifest.id, { id: 'community.example.todocontext.openCount', order: 100, // Async because we read the workspace. Returns null when there is nothing useful. collect: async function () { // ctx.query is the read-only workspace query surface (see "The ctx API"). const page = await ctx.query.queryMetadata({ itemType: 'todo', limit: 500 }) const open = page.rows.filter(function (r) { return !r.dueDate || r.dueDate >= todayIso() }) if (open.length === 0) return null return { stickyFacts: [ { tier: 'important', text: 'The user has ' + open.length + ' open todo item(s) of type "todo". ' + 'Use the todo items, not notes, when asked about tasks.', }, ], } }, }) return function deactivate() {} } function todayIso() { return new Date().toISOString().slice(0, 10) } ``` Drop it at `/.undra/extensions/todocontext/extension.js`, restart Undra, then ask an agent about your tasks. Your sticky fact is now part of the prompt, so the model knows the count and which item type to trust. :::tip Prefer `collectSync` for cheap facts If your fact does not need any I/O (it is a constant, or something you already hold in a captured variable), put it in `collectSync` so it shows up on the fast preview path too. Provide `collect` only when you must await something. You can provide both, the host calls the right one for each path. ::: ## Where to go next - [The ctx API](/docs/extensions/ctx-api/), the `workspace` and `query` surfaces your handlers and providers call into. - [Item types](/docs/extensions/item-types/), give your data a real type so context providers have something concrete to describe. - [Packaging & distribution](/docs/extensions/packaging/), bundle and ship what you built here.