Writing a plugin
A plugin is a folder that adds to Atlas. Its format is Claude Code’s, so a plugin written for
Claude Code (skills, commands, agents, hooks, MCP servers, programs, options and monitors) runs in
Atlas as it is. What only Atlas reads
sits under atlas in the same manifest. The decisions behind it are in
ARCHITECTURE.md, Plugins.
Start one
atlas plugins new my-plugin # in the home's plugins/, where Atlas finds itatlas plugins validate ~/.atlas/plugins/my-pluginatlas plugins new my-plugin --folder ~/code # or in a folder of yours, installed from therenew writes the smallest plugin that works: a manifest, a tool and a skill. validate loads it
as Atlas does, in a home of its own that is thrown away after, and says what it registers and what
is wrong. Then turn it on in the app’s Plugins screen (or atlas plugins enable my-plugin): Atlas
shows what it declares, and it runs only once you consent. A plugin that declares something new
after an update is turned off until you consent again.
A plugin’s Python runs inside Atlas with your permissions. There is no sandbox.
Atlas writes one with you, too: ask it in a conversation, or type /write-plugin and what it
should do. The skill Atlas ships for it reads this guide, makes the plugin in the conversation’s
folder, checks it with validate and installs it, asking you first; you turn it on.
The folder
my-plugin/├── .claude-plugin/│ └── plugin.json # the manifest; `name` is the only field required├── __init__.py # optional: register(ctx), for tools, hooks, channels and executors├── skills/ # optional: one folder per skill, each with a SKILL.md│ └── my-plugin/│ └── SKILL.md├── commands/ # optional: one Markdown file per command├── agents/ # optional: one Markdown file per subagent├── hooks/hooks.json # optional: commands run at the conversation's events├── monitors/monitors.json # optional: commands run in the background of each conversation├── bin/ # optional: programs the model's commands find by name└── .mcp.json # optional: the MCP servers it bringsThe folder’s name is the plugin’s name, in kebab-case. A plugin’s own files live in its folder;
what it keeps while it runs (caches, node_modules, renders) goes in its data folder,
ctx.data_folder, which an update does not touch and a backup carries.
The manifest
{ "name": "my-plugin", "version": "0.1.0", "description": "What it adds to Atlas, in one line.", "author": { "name": "you" }, "atlas": { "api": 14, "tools": ["my_plugin_hello"], "secrets": [ { "name": "MY_SERVICE_TOKEN", "description": "Where to get it.", "url": "https://example.com" } ], "settings": [{ "name": "api_url", "default": "https://example.com", "description": "Where." }], "network": ["example.com"] }}apiis the SDK version the plugin is written for. Tools need 14. Atlas refuses a plugin newer than itself.tools,channelsandexecutorsname what its Python registers. Registering anything it does not name fails the plugin.secretsare read from the vault, and only those named (ctx.secret). The user stores them on the Plugins screen, or withatlas plugins secret.settingsare values the user may change, with their defaults (ctx.setting).networklists the hosts it reaches. It is shown at consent and not enforced.hooksnames the events its Python hooks into (needsapi15).readersnames the kinds of attachment its Python reads into text,audio(needsapi21).desktopnames what its screen does outside the app’s window (needsapi23 and a screen):shortcut, a key heard in every program, andpaste, text pasted into the program in focus.
Claude Code’s userConfig works too: each option is a setting, and a sensitive one goes to the
vault as <PLUGIN>_<KEY> (erp and apiToken: ERP_API_TOKEN). An MCP server reads an option
as ${user_config.KEY}; a hook gets each as CLAUDE_PLUGIN_OPTION_<KEY>, and an exec-form hook
as ${user_config.KEY} in its args. A monitor’s command takes none, as in Claude Code.
Skills
Each folder of skills/ with a SKILL.md is a skill, in Claude Code’s format (YAML frontmatter
with name and description, then the instructions). While the plugin is on, the model lists it
after your own skills, named after the plugin: my-plugin:my-plugin. You can invoke it as
/my-plugin:my-plugin too. Atlas never changes a plugin’s skill; change it in the plugin.
Commands
Each Markdown file of commands/ is a command, Claude Code’s older form of a skill: named by its
file (review.md is /my-plugin:review), its frontmatter optional, its description (or its
first line) saying what it does. A skill of the plugin with the same name wins.
Agents
Each Markdown file of agents/ is a predefined subagent: a helper for one kind of task, with no
memory of its own. delegate offers it by name (my-plugin:reviewer), and a task’s agent
runs it.
---name: reviewerdescription: Reviews a change for bugs before it is merged.tools: Read, Grep, Bashmodel: openrouter/anthropic/claude-sonnet-5-5---You review changes. Say only what is wrong, with the file and the line.- Its instructions come before the task’s context.
toolskeeps Atlas’s own tools to those named; Claude Code’s names work (Bashisrun_command,Readisread_file,GrepandGlobarefind_files). Without it, it has every tool a subagent has.modelisprovider/model. Claude Code’s aliases (sonnet,inherit) run on the parent’s model.
Hooks
hooks/hooks.json is Claude Code’s: a command runs at an event, gets the event’s JSON on its
stdin, and may stop what happens, change it or add context.
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "python", "args": ["${CLAUDE_PLUGIN_ROOT}/guard.py"] }] } ] }}- The events Atlas fires:
SessionStart,UserPromptSubmit,PreToolUse,PostToolUse,PostToolUseFailureandStop. A hook on another of Claude Code’s events is read and never runs. - A matcher names Atlas’s tool (
run_command) or Claude Code’s (Bash). - The answer is Claude Code’s: exit 2 blocks, with stderr as the reason; or JSON on stdout
(
decision,hookSpecificOutputwithpermissionDecision,updatedInput,additionalContext). allowlowers no approval.denyblocks andaskmakes Atlas ask, but the user’s gate still decides whatallowwould skip.
Python does the same with ctx.register_hook(event, run, matcher=...): run gets the same JSON
as a dict and returns the same answer, or None.
Programs
The files of bin/ are on the PATH of the commands the model runs while the plugin is on, so
the model runs them by name. On Windows the commands run in PowerShell: give a program a .cmd
or .ps1 beside the script bash runs.
Monitors
monitors/monitors.json is Claude Code’s: commands that run in the background of each of your
conversations, each line they print reaching the model as a notice, which it answers.
[ { "name": "deploys", "command": "\"${CLAUDE_PLUGIN_ROOT}\"/watch-deploys.sh", "description": "Deploy status changes", "when": "on-skill-invoke:deploy" }]whenisalways(from the conversation’s first turn) oron-skill-invoke:<skill>(the first time that skill is used).- The lines that come within a second reach the model as one notice. Each notice is a turn, and a turn costs a model call: print only what the model should act on.
- It stops with the conversation, and when the plugins change.
MCP servers
.mcp.json holds Claude Code’s mcpServers. Atlas names each server after the plugin
(plugin_<plugin>_<server>, its tools mcp__plugin_<plugin>_<server>__<tool>). Each server
connects when the plugin is turned on, and stops when it is turned off.
${CLAUDE_PLUGIN_ROOT}is the plugin’s folder, and${CLAUDE_PLUGIN_DATA}its data folder.${NAME}is a secret from the vault. Naming it declares it, asatlas.secretsdoes: Atlas shows it when you turn the plugin on, so a plugin never reads a key you were not shown. Declare it underatlas.secretsanyway, to say where to get it."trust": "untrusted"makes every call that may write wait for your approval, as in your ownmcp.json.
An MCP server’s tools reach the model behind the tool search, like every MCP tool.
Tools
from typing import Any
from atlas.plugins.sdk import PluginContext, ToolHost
def register(ctx: PluginContext) -> None: async def hello(arguments: dict[str, Any], host: ToolHost) -> str: host.progress("Saying hello") # shown under the call, never to the model return f"Hello, {arguments['name']}!" # what the model reads
ctx.register_tool( "my_plugin_hello", "Say hello to someone by name.", {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}, hello, read_only=True, # it changes nothing, so it may run beside other read-only calls )- Import only
atlas.plugins.sdkfrom Atlas. It is the contract, and it only grows: nothing in it is removed or renamed. registeronly registers. It runs once when the plugin is turned on, with 10 seconds to finish. The work starts when the model calls the tool.- The model is told the schema, but nothing checks the arguments against it. Read them with care.
- A tool that raises fails that call, and the model reads the message. The plugin stays on.
- When the user stops the turn, the call is cancelled. End what it started.
- The approval gate is Atlas’s, the same as for its own tools. A tool named as one of Atlas’s is left out.
await host.send_file(path)sends the user a file ("api": 22): Atlas keeps a copy in the home, shows it in the conversation, and sends it in the chat when the conversation is a channel’s. It returns where the home keeps it. A channel sends files with its ownsend_file(chat, path, media_type); one without it says the file waits in the app.
What stops with it
studio = start_studio() ctx.on_unload(studio.terminate)A stop runs when the plugin is turned off, when Atlas loads it again, and when Atlas closes. It
may be async. A stop that raises is logged, and the others still run.
Readers
A reader turns one kind of attachment into text before the model reads it: the voice plugin
writes down what an audio file says. Declare it under atlas.readers ("api": 21), and register
it:
from pathlib import Path
def register(ctx: PluginContext) -> None: async def read(path: Path, media_type: str) -> str: return await my_speech_service(path.read_bytes(), media_type)
ctx.register_reader("audio", read)- It reads every such file the user sends, dropped in the app or sent on a channel, before the turn. What it returns goes to the model in the file’s note, and Atlas keeps it with the file, so each is read once.
- One reader a kind: when two plugins read audio, the first loaded reads it, and the other is logged.
- It has 2 minutes. One that raises, returns nothing or runs out leaves the file’s note as it was, and the turn goes on.
audiois the only kind for now.
Channels
A channel lets people talk to Atlas from somewhere else: a chat app, a phone line, a mailbox. It is
one more client of Atlas, as the app is. Who may talk (pairing), the commands (/new, /stop),
which conversation a chat goes on and who answers a question are Atlas’s, the same for every
channel. The channel only carries text, buttons and files.
"atlas": { "api": 22, "channels": ["my-chat"], "secrets": [{ "name": "MY_CHAT_TOKEN" }] }from pathlib import Path
from atlas.plugins.sdk import ChannelHost, Inbound, PluginContext, Prompt
class MyChat: def __init__(self, token: str) -> None: self.token = token
async def run(self, host: ChannelHost) -> None: async for message in my_service.listen(self.token): # until Atlas cancels it await host.receive( Inbound( chat=message.chat, user=message.user_id, name=message.user_name, text=message.text, ) )
async def send(self, chat: str, text: str) -> None: await my_service.say(chat, text) # Markdown, as the models write it
async def working(self, chat: str, busy: bool) -> None: await my_service.typing(chat, busy)
async def ask(self, chat: str, prompt: Prompt) -> None: await my_service.buttons(chat, prompt.id, prompt.text, prompt.choices) # A press calls `await host.choose(prompt.id, index, user)`; `None` means it was taken.
async def settle(self, chat: str, prompt: str, said: str) -> None: await my_service.replace_buttons(chat, prompt, said)
async def send_file(self, chat: str, path: Path, media_type: str) -> None: await my_service.upload(chat, path, media_type)
def register(ctx: PluginContext) -> None: ctx.register_channel("my-chat", lambda: MyChat(ctx.secret("MY_CHAT_TOKEN") or ""))makeruns each time the channel starts, so it reads its secrets then. A channel starts when the plugin is on and stops when it is turned off.runreceives until it is cancelled. It returns or raises only when it cannot go on; Atlas says so and does not start it again.host.failed(reason)says why first (a refused token), on the Plugins screen.Inbound.useris a stable id, never a name: pairing lets people in by it.nameis what the user sees when someone asks to pair.- Files people send go in
Inbound.files("api": 7), each anInboundFilewith itsnameand its bytes (data). What the model reads of them is Atlas’s, as for a file dropped in the app. send_file("api": 22) sends a file a tool made; a channel without it gets a line naming the file instead.- The Telegram plugin Atlas ships is a whole channel: src/atlas/plugins/bundled/telegram.
Executors
An executor brings an outside harness to the workbench: a coding agent installed on the computer, which the user and Atlas hand work to, each session on a card. It only carries, as a channel does: it starts the harness, reads what the harness says into events, and writes back the answers Atlas gives. Who answers a question or a permission (the user on the card, the guardian, the conversation’s model) is Atlas’s, and the plugin never learns which.
"atlas": { "api": 18, "executors": ["my-harness"] }from pathlib import Path
from atlas.plugins.sdk import ( Capabilities, Detection, Executor, ExecutorHost, ExecutorInstance, ExecutorSpec, PluginContext, Transcript,)
class MyHarness: title = "My harness" # how the screen and `delegate` call it
async def detect(self) -> Detection: return Detection(installed=True, version="1.2.0", supported=True)
def capabilities(self) -> Capabilities: return Capabilities(questions=False, permissions=True, steer=False, auto=False)
async def start(self, spec: ExecutorSpec, host: ExecutorHost) -> ExecutorInstance: return await MySession.start(spec.folder, host) # raises ExecutorError when it cannot
async def transcript(self, session: str, folder: Path) -> Transcript: return Transcript(entries=read_my_log(session, folder))
def register(ctx: PluginContext) -> None: def make() -> Executor: return MyHarness()
ctx.register_executor("my-harness", make)-
Executorneedstitle,detect(installed, its version, itsAccount, with no effect on it),capabilities,startandtranscript. The rest is optional, each behind a flag ofCapabilitiesthat says it is there; Atlas never calls a method whose flag is off:CapabilitiesWhat it offers apiquestions,permissionsit asks over its channel ( ExecutorHost.ask,approve)2 steer,autoExecutorInstance.steer,set_auto2 accountsExecutor.log_in, the harness’s own login in the browser2 sessions,forkExecutor.sessions,SessionInfo;ExecutorSpec.fork3 modelsExecutor.models,ModelChoice;ExecutorInstance.set_model4 connectorsExecutor.connectors,Connector5 foldersExecutor.folders,SessionFolder8 usageExecutor.usage,PlanUsage,UsageWindow,UsageChanged9 commandsExecutor.commands,SlashCommand,CommandsChanged10 account_foldersExecutor.account_folder,new_account_folder,for_account;ExecutorSpec.resume_from12 delete_sessionsExecutor.delete_session12 filesExecutorInstance.prompt(files=),ExecutorFile,ExecutorSpec.directories13 -
ExecutorInstanceis one running harness:promptruns a turn until itsFinished(Atlas never calls it while a turn runs), andstopends it with every process it started. -
What happens is an event, handed to
host.event:Started(the session’s id),Said,Thought,CalledandReturned(a tool call and its result),Compacted,LimitReached,UsageChanged,CommandsChanged, andFinished, which ends the turn. -
A question or a permission waits on
host:await host.ask([Question(...)])returns the answers,await host.approve(Permission(...))aVerdict. Both wait as long as the answer takes.host.failed(reason)says the instance cannot go on. -
transcriptreads a session back, wherever the harness keeps it, in one shape for every harness:TranscriptEntryitems in aTranscript. -
A harness that speaks the Agent Client Protocol needs no channel of its own:
atlas.plugins.acp("api": 18) starts it, speaks ACP over its stdio and turns its updates into these events. The Gemini CLI and DeepSeek plugins Atlas ships use it: src/atlas/plugins/bundled/gemini-cli, src/atlas/plugins/bundled/deepseek. The Claude Code and Codex plugins speak their harness’s own protocol.
Two examples
A business system’s MCP, no Python. The system already has an MCP server, so the plugin only points at it and teaches its use:
{ "name": "erp", "description": "The company's ERP: orders, stock and invoices.", "atlas": { "secrets": [{ "name": "ERP_TOKEN", "description": "An API token from the ERP's settings." }] }}{ "mcpServers": { "erp": { "type": "http", "url": "https://erp.example.com/mcp", "headers": { "Authorization": "Bearer ${ERP_TOKEN}" } } }}Its skills/ holds how the company works (“closing the month”, “a new order”), so the model
calls the right tools in the right order.
Video with Remotion. The plugin brings:
- Remotion’s agent skills, to write compositions well;
- a skill that says to hand the composition to Claude Code through
delegatewhen Claude Code is installed, and to write it directly otherwise; - a
video_rendertool that runsnpx remotion renderin the project, reports its progress, and returns the file; - a stop that ends the Remotion Studio.
The Studio is a local server, so the model shows it in the side panel with desktop_preview.
Remotion is free for individuals and companies of up to three people. A product that makes
videos for its users pays per render.
Commands with code, terminal commands, your model and your conversations
Version 16 of the SDK ("api": 16). Each is named in the manifest:
"atlas": { "api": 16, "commands": ["status"], "cli": ["video-render"], "llm": true, "notify": true}def register(ctx: PluginContext) -> None: async def status(text: str, host: CommandHost) -> str: return "3 renders running." # the reply, with no model call
def render(argv: list[str]) -> int: print("rendering", argv) # it writes to the terminal itself return 0 # the exit code
async def summary(text: str, host: CommandHost) -> str: said = await ctx.llm.complete(text, system="Two lines at most.") return said.text
ctx.register_command("status", status, "What is rendering.") # /video:status ctx.register_cli_command( "video-render", render, "Render from the terminal." ) # atlas video-render ctx.register_command("summary", summary, "Summarize with your model.")- A slash command is typed
/<plugin>:<name>. What it returns is the reply, and it stays in the conversation, so the model reads it at the next turn. - A terminal command gets the arguments as typed. A name Atlas uses is never a plugin’s.
ctx.llm.completeuses the model your conversations use when they pick none, and each call costs your credit:atlas usagelists it asplugin:<name>. Withjson_schema=,said.parsedholds the JSON, orNonewhen the model did not give JSON that fits.await ctx.notify(conversation, text)brings text to a conversation, where it starts a turn. A tool knows its conversation ashost.conversation, so a render that ends later can say so there.
Model providers
A plugin can add a model provider, written as models.toml writes one:
"atlas": { "providers": { "my-vllm": { "name": "My vLLM", "base_url": "http://192.168.0.5:8000/v1", "key": "MY_VLLM_KEY" } }}It shows in the model picker once its key is in the vault, and leaves when the plugin is turned off. A plugin never changes a provider Atlas or you already have.
A screen of its own
desktop/plugin.js is the plugin’s screen in the app: plain JavaScript (no JSX, no build), loaded
as it is. It imports only @atlas/plugin-sdk, react and react/jsx-runtime.
import { host, useValue } from '@atlas/plugin-sdk'import { useState } from 'react'import { jsx, jsxs } from 'react/jsx-runtime'
function Page({ ctx }) { const [renders, setRenders] = useState(null) return jsxs('div', { children: [ jsx('button', { onClick: async () => setRenders(await ctx.call('renders')), children: 'Refresh' }), jsx('pre', { children: JSON.stringify(renders, null, 2) }) ] })}
export default { id: 'video', // the plugin's name register(ctx) { ctx.register({ area: 'page', id: 'renders', title: 'Renders', render: () => jsx(Page, { ctx }) }) ctx.register({ area: 'panel', id: 'now', title: 'Video', render: () => jsx('p', { children: 'Ready.' }) }) }}area: 'page'is a page of the main area, with an entry in the sidebar;area: 'panel'is a tab of the side panel;area: 'box'is a button beside the words of the conversation’s box, as the voice plugin’s microphone.host.insertText(text)puts words after what the conversation’s box holds, for the user to read and send (a dictation);host.send(text)sends a message as the box does, making the conversation when none is open.- With
desktopdeclared ("api": 23):ctx.shortcut(id, accelerator, run)holds a key heard in every program while the app is open, named as Electron names keys (Capslock,F9,Control+Shift+Space), and resolves''or why it cannot be held;ctx.paste(text)types the text where the app’s cursor is when its window has the focus (app), else pastes it in the program in focus (pasted), or leaves it on the clipboard when that program takes no keys (copied).ctx.bar({ text, tone })shows a floating bar that never takes the focus (tone:recording,working,done,error),nullhides it. The voice plugin’s dictation anywhere uses all three. ctx.call(name, params)calls the plugin’s Python, which registers it withctx.register_call(name, run)and names it underatlas.calls("api": 17). It runs where Atlas runs, also when the app is on another machine.host.state.conversation,host.state.connectionandhost.state.homeare the app’s state, read withuseValue(...);host.request(method, params)calls the server as the app does.ctx.onEvent(type, listener),ctx.setInterval,ctx.storageandctx.onDisposeare taken away with the plugin.- The screen has all the app’s access. It is shown at consent.
While you write it
Save, and Atlas loads it again: its Python, its skills, its hooks and its screen, in the app that
is open. A save that breaks it says why in the Plugins screen; the next save that fixes it runs
again. The Plugins screen’s Reload does the same on asking. Keep what the plugin writes in its
data folder (${CLAUDE_PLUGIN_DATA}), not in its own: a plugin writing in its folder loads itself
again and again.
Plugins it needs
{ "name": "video", "dependencies": ["fonts", { "name": "render-kit", "version": "~2.1.0" }]}The version is an npm range (~2.1.0, ^2.0, >=1.4). The plugin turns on only after those it
needs, and stops when one of them stops, saying which. They load first.
Installing one by its address
atlas plugins install owner/repo # GitHubatlas plugins install https://host/team/plugins.git#video --ref v2.1.0atlas plugins install ~/code/video # a folder on this computer, copiedatlas plugins update video # again from where it cameatlas plugins remove video # its data staysIt arrives off; turn it on with consent, as any other. An update that declares something else waits for your consent again.
Publishing it in the marketplace
The marketplace is a public repository, marqueskaio/atlas-marketplace: every Atlas lists what it
holds, in the app’s Marketplace and with atlas marketplace list. An item enters by a pull request
that adds one file, items/<name>.yaml, and the maintainer reviews it.
name: video # lowercase letters, digits and dashes; the file is named after itkind: plugin # skill, mcp, plugin or executortitle: Videosummary: Cut and render videos with Remotion.author: your-github-accountlicense: MITrepo: your-github-account/atlas-videosha: 0123456789abcdef0123456789abcdef01234567 # the whole commit that is reviewedpath: plugins/video # the folder in the repository; leave it out for the rootversion: "1.0.0" # what you call it; the commit is what installsimage: https://raw.githubusercontent.com/your-github-account/atlas-video/<sha>/cover.png-
Pick the smallest kind. A skill alone is a
skill, one folder with itsSKILL.md; one MCP server is anmcpitem; a plugin that registers an executor is anexecutor; anything that brings more (skills with a server of its own, hooks, a screen, Python) is aplugin. The marketplace’s CI refuses an item whose code is not the kind it says. -
A skill says what it needs in its own frontmatter, and the marketplace checks it on the user’s machine before installing:
metadata:requires:bins: [ffmpeg] # programs it runs; said when missing, installed anywaymcp: [docs] # MCP servers it calls: items of the marketplace, brought alongos: [linux, macos] # where it works; refused elsewhereIts scripts carry their own libraries: a Python script declares them in its
# /// scriptheader and runs withuv run; a Node package runs withnpxat an exact version. A skill brings no secret, since the commands the model runs never see the vault: what needs a key goes through an MCP server, or is a plugin. -
An MCP server is its
mcp.jsonentry underserver, with each${NAME}it reads undersecrets(a name, what it is, the page that makes one). What it runs is pinned:npxwithname@1.2.3,uvxwithname==1.2.3,docker runwith an image by its digest, or a remotehttpsaddress. Servers that sign in with OAuth wait until Atlas does. -
A new version is a new pull request that moves
sha; the reviewer reads what changed between the two commits. Your code must not fetch and replace itself, nor carry compiled files. What it does on the network, in the background or outside its own folder goes in its README. -
Check it first, as the CI does:
Terminal window atlas marketplace check items/video.yaml # the file, and its code at its commitatlas marketplace check items/video.yaml --load # and its Python loaded, in a home thrown away--loadruns the plugin’s Python: use it on your own plugin, or where foreign code may run.
What the SDK offers
Everything a plugin’s Python may import is in atlas.plugins.sdk (API_VERSION 23), and the ACP
client in atlas.plugins.acp. Everything a screen may import is in @atlas/plugin-sdk
(SCREEN_API 3). Each name below has its section above. A test keeps this list whole: a name the
SDK adds and the guide does not name fails it.
register(ctx) gets a PluginContext:
| Member | What it is | api |
|---|---|---|
ctx.name, ctx.data_folder, ctx.logger |
its name, its data folder, its log | 1 |
ctx.secret(name), ctx.setting(name) |
a declared secret from the vault, a declared setting | 1 |
ctx.register_channel(name, make) |
a channel (Channels) | 1 |
ctx.register_executor(name, make) |
an executor (Executors) | 2 |
ctx.register_tool(name, description, parameters, run, read_only=) |
a tool (Tools) | 14 |
ctx.on_unload(stop) |
what stops with it | 14 |
ctx.register_hook(event, run, matcher=) |
a hook (Hooks) | 15 |
ctx.register_command(name, run, description, hint=) |
a slash command | 16 |
ctx.register_cli_command(name, run, description) |
a terminal command | 16 |
ctx.llm.complete(prompt, system=, max_tokens=, json_schema=) |
the user’s model: LlmMessage, LlmResult, LlmError, PluginLlm |
16 |
ctx.notify(conversation, text) |
a message that starts a turn | 16 |
ctx.register_call(name, run) |
what the screen’s ctx.call reaches |
17 |
ctx.register_reader(kind, run) |
an attachment read into text (Readers) | 21 |
What Atlas hands what it registered:
- A tool’s
ToolHost:progress(text),conversation,send_file(path)(22). - A command’s
CommandHost:conversation. - A channel’s
ChannelHost:receive,choose,failed; the channel is aChannel(run,send,working,ask,settle), or aFileChannelwithsend_file; it hands over anInboundwithInboundFiles, and shows aPrompt. - An executor’s
ExecutorHost:event,ask,approve,failed. The executor is anExecutorthat starts anExecutorInstance, withExecutorSpec,Capabilities,Detection,Account,Question,Option,Permission,Verdict,Transcript,TranscriptEntry,SessionInfo,SessionFolder,ModelChoice,Connector,SlashCommand,PlanUsage,UsageWindow,ExecutorFileandExecutorError; its events areStarted,Said,Thought,Called,Returned,LimitReached,Compacted,Finished,UsageChangedandCommandsChanged.
A screen’s register(ctx) gets: ctx.plugin, ctx.register, ctx.call, ctx.onEvent,
ctx.onDispose, ctx.setInterval, ctx.setTimeout, ctx.storage (get, set, remove), and
with desktop declared ctx.shortcut, ctx.paste and ctx.bar. From @atlas/plugin-sdk it
imports useValue and host: host.state (connection, conversation, home),
host.request, host.openPage, host.send, host.insertText and host.togglePanel.
Not yet
Atlas lists a plugin that brings any of these, and refuses to turn it on: LSP servers, output styles, themes and workflows. A screen has a page and a panel; a status bar, a palette and themes are not in the app.