Build for Vanish.
One object, the whole app.
Every extension is handed a single VanishAPI object, wired straight into the running application. Read state, register tools and views, drive the agent, paint the terminal — this page is the entire contract, nothing held back.
Getting started
onLoad(api)An extension is a module that exports lifecycle hooks. Each hook receives the VanishAPI object. That is the only thing you need to know to write your first one.
Export a default object implementing the hooks you care about — onLoad, onUnload, onActivate, onDeactivate. Logic-only extensions can live in a functions/ folder instead; a default function there is treated as onLoad(api).
import type { VanishAPI } from 'vanish-tui';
export default {
async onLoad(api: VanishAPI) {
api.log('My extension has been loaded!');
// reach into any subsystem below
},
async onUnload(api: VanishAPI) {
api.log('My extension is being unloaded!');
},
};A small manifest.json sits next to it. Declare the permissions you need up front — the user grants them under Settings › General › Security & Permissions.
{
"name": "my_extension",
"displayName": "My Extension",
"version": "1.0.0",
"description": "Does one small thing well.",
"main": "index.ts",
"permissions": ["agent"],
"dependencies": []
}Registration methods (registerTool, registerSections, registerCommand, subscribe, on…) hand back an unregister function. Call them from onLoad, keep the disposers, and run them in onUnload so your extension unloads cleanly with no leaks.
22 subsystems, grouped six ways. Jump to any of them — or use the filter in the sidebar.
Foundation
The wiring every extension leans on — the event bus, the log buffer, and durable storage.
Events
api.eventsSubscribe to and emit application-wide events.
The event system lets your extension react to changes across the whole app — the theme flips, extensions reload, a view comes into focus, a chat thread starts streaming. It is the spine that most other subsystems publish onto.
api.events.on(event, listener)api.events.off(event, listener)api.events.emit(event, ...args)api.events.once(event, listener)Common core events
themeChangedname: stringThe active theme changed.llmBusybusy: booleanThe active LLM started or stopped streaming.extensionsReloaded—Extensions reloaded from the active directory.viewActivatedid: stringA view became the active view.viewEnabledChanged{ viewId, enabled }A view was enabled or disabled.chatSessionActivated{ sessionId }The active chat session changed.chatThreadStarted{ threadId, sessionId, … }A chat request started streaming.chatThreadChunk{ threadId, content, delta }Streamed content accumulated for a session.chatThreadCompleted{ threadId, cancelled, message }A chat response finished.mouse{ x, y, button, action, … }Terminal mouse activity, when tracking is on.api.events.on('themeChanged', (themeName: string) => {
api.log(`Theme was just changed to ${themeName}!`);
});Logging
api.logRead, subscribe, and write to the application log buffer.
api.log is both a function and a namespace. Call it to write a line (auto-prefixed with [Extension]); reach for its members to write at a specific level or to read the live buffer.
Writing
api.log(message)api.log.info(message)api.log.warn(message)api.log.error(message)api.log.success(message)api.log.system(message)Reading & subscribing
api.log.getRecent(limit?)→ LogEntry[]api.log.subscribe(listener)→ () => voidinterface LogEntry {
id: number;
level: 'log' | 'info' | 'warn' | 'error' | 'success' | 'system';
message: string;
timestamp: number; // Unix epoch milliseconds
}const recentErrors = api.log
.getRecent(100)
.filter((entry) => entry.level === 'error');
const unsubscribe = api.log.subscribe((entry) => {
if (entry.level === 'warn' || entry.level === 'error') {
// react to errors or warnings
}
});Data & channels
api.dataPersist extension-owned files and coordinate live shared channels.
Two different needs, one namespace. Use file storage for artifacts that must survive a restart (indexes, snapshots, caches). Use shared channels for live cross-extension state that only needs to exist while Vanish is running — no globalThis, no temp files, no handshake events.
File storage
api.data.getRootDir()→ stringapi.data.saveData(path, value)api.data.readData<T>(path)api.data.editData<T>(path, editor)api.data.deleteData(path)Shared channels
Channels are in-memory only. Each publisher namespace retains its latest value per channel, and subscribers receive the current snapshot immediately by default. Unloading an extension clears its retained publications.
api.data.publish(channel, value)api.data.unpublish(channel)api.data.readChannel<T>(channel)→ SharedChannelEntry<T>[]api.data.subscribe<T>(channel, listener, options?)→ () => voidawait api.data.editData('index.json', (current) => {
const base = current && typeof current !== 'string'
? current
: { version: 1, sessions: [] };
return {
...base,
sessions: [...base.sessions, { id: 'chat-123', title: 'New Session' }],
};
});api.data.publish('vanish:shared:hud-weather', {
summary: '72F Clear',
locationLabel: 'Brooklyn, NY',
updatedAt: Date.now(),
});
const unsubscribe = api.data.subscribe('vanish:shared:hud-weather', (entries) => {
const latest = entries.at(-1);
api.log(`Current weather: ${latest?.value.summary ?? 'none'}`);
});Rendering & motion
Draw to the terminal buffer, animate frame-by-frame, and surface overlay notifications.
Renderer
api.rendererPaint cells and text directly onto the terminal buffer.
Low-level access to the terminal buffer for effects that aren't tied to a single view — transitions, floating progress bars, notification badges, persistent HUDs.
api.renderer.getSize()→ { width, height }api.renderer.setCell(x, y, cell)api.renderer.drawText(x, y, text, options)api.renderer.registerOverlay(callback)→ () => voidapi.renderer.reserveSpace(top, bottom)api.renderer.forceFullRender()export default function (api: VanishAPI) {
api.events.on('themeChanged', () => {
let opacity = 1.0;
const cleanup = api.animator.onFrame(() => {
opacity -= 0.1;
const { width, height } = api.renderer.getSize();
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
api.renderer.setCell(x, y, { char: ' ', bg: '#ffffff' });
}
}
if (opacity <= 0) {
cleanup();
api.renderer.forceFullRender();
}
});
});
}Animation
api.animatorDrive smooth, frame-by-frame transitions.
Vanish ships an animation engine that runs on every render frame. Pair api.animator with the easing library to make movement and color fades feel natural.
api.animatorapi.easingconst progress = api.easing.easeOutQuart(elapsedTime / totalDuration);Startup
api.startupCustomize the ASCII artwork shown on launch.
The startup animation shows ASCII art in the center and a progress bar at the bottom. Swap the art for your own — keep all lines a similar length for clean centering.
api.startup.getArtwork()→ string[]api.startup.setArtwork(artwork)export default function (api: VanishAPI) {
api.startup.setArtwork([
' __ __ _ __ __ ',
'| \/ | | | | \/ |',
'| \ / | ___ | |__ | \ / |',
'| |\/| |/ _ \| \'_ \| |\/| |',
'| | | | (_) | | | | | | |',
'|_| |_|\___/|_| |_|_| |_|',
]);
}Notifications
api.notificationsOverlay notifications with animations, themes, and actions.
Send overlay notifications that float above every view. They support custom positions, animations, themes, sizes, semantic tones, and action buttons.
api.notifications.send(options)→ NotificationHandleapi.notifications.dismiss(id)api.notifications.dismissAll()api.notifications.getActive()→ NotificationInfo[]interface NotificationOptions {
title?: string;
message: string; // required
duration?: number; // ms; 0 = manual dismiss
position?: NotificationPosition;
animation?: string | NotificationAnimation;
theme?: string | NotificationTheme;
width?: number;
height?: number;
icon?: string;
tone?: 'info' | 'success' | 'warning' | 'error';
onDismiss?: () => void;
dismissible?: boolean; // default: true
actions?: NotificationAction[];
}const handle = api.notifications.send({
title: 'Download in Progress',
message: 'Downloading update... 45%',
tone: 'info',
duration: 0, // never auto-dismiss
});
handle.update({ message: 'Downloading update... 78%' });
handle.dismiss();Animations, themes & sizing
Use built-ins or register your own. Built-in animations: slide, fade, pop, bounce, drop, zoom, none. Built-in themes: default, minimal, accent, glass, sharp, borderless, toast.
api.notifications.animations.register(name, animation)api.notifications.themes.register(name, theme)api.notifications.size.setDefaults(config)api.notifications.send({
title: 'Search Extension',
message: 'Agent permission is required for web search tools.',
tone: 'warning',
duration: 0,
actions: [
{
id: 'permission-requests',
label: 'Permission requests',
shortcut: "Ctrl+'",
onPress: () => api.events.emit('permissionRequests:open'),
},
],
});Chat & models
Orchestrate chat sessions and talk to the configured LLM providers.
Chat sessions
api.chatCreate, restore, fork sessions and substitute message rendering.
api.chat is session orchestration. Create new chats, restore saved conversations, switch the active session, and submit prompts without adding a view of your own. It also exposes one narrow rendering hook — registerMessageRenderer — that lets you substitute a message's body lines while the chat view keeps owning layout, scroll, and chrome.
api.chat.listSessions()→ ChatSessionSummary[]api.chat.getSession(id)api.chat.getActiveSessionId()api.chat.createSession(options?)→ ChatSessionSummaryapi.chat.activateSession(id)api.chat.setSessionTitle(id, title)api.chat.sendMessage(content, options?)→ { accepted, sessionId, threadId }api.chat.deleteSession(id)api.chat.forkSession(id, options)→ ForkChatSessionResult | nullapi.chat.registerMessageRenderer(renderer)→ () => voidapi.chat.appendToComposer(text)api.chat.getAreaGeometry()→ ChatAreaGeometry | nullinterface ChatSessionSummary {
id: string;
title: string;
createdAt: string;
updatedAt: string;
messageCount: number;
isStreaming: boolean;
providerName: string | null;
providerDisplayName: string | null;
model: string | null;
preview: string;
}const session = api.chat.createSession({ title: 'Refactor Notes', activate: true });
const result = api.chat.sendMessage('Summarize the tradeoffs of this refactor.', {
sessionId: session.id,
});
if (result.accepted) {
await api.views.activate('chat');
}Message renderers
const off = api.chat.registerMessageRenderer({
id: 'demo-bullets',
priority: 10,
filter: (m) => m.role === 'assistant',
render: (m, ctx) => {
const lines = ctx.defaultRender();
if (lines.length === 0) return null;
lines[0] = { ...lines[0], text: '• ' + lines[0].text };
return lines;
},
});LLM providers
api.llmllmQuery providers, read model presets, and run completions.
Vanish integrates multiple providers (Groq, Google AI Studio, and more). Read the active provider for low-level generation, or — better — use the preset completion APIs that resolve the right provider and model from the user's four configured presets (heavy, medium, chat, lite).
Provider access
api.llm.getProvider()→ LLMProviderapi.llm.setProvider(name)api.llm.listProviders()api.llm.isBusy()Model presets
api.llm.getPresetModel(preset)api.llm.setPresetModel(preset, model)api.llm.getPresets()Direct completion APIs
The convenient path. api.llm.response.{heavy,medium,chat,lite}(messages, options?) returns a Promise<CompletionResult>; api.llm.stream.{…} returns an AsyncGenerator<StreamChunk>. If a preset or provider isn't configured, a notification is shown to the user and an error is thrown.
interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface CompletionOptions {
temperature?: number;
maxTokens?: number;
topP?: number;
stop?: string[];
}
interface CompletionResult {
content: string;
finishReason?: 'stop' | 'length' | 'content_filter';
usage?: { promptTokens: number; completionTokens: number; totalTokens: number };
}try {
const result = await api.llm.response.heavy([
{ role: 'system', content: 'You are a code reviewer.' },
{ role: 'user', content: 'Review: function add(a,b){return a+b}' },
], { temperature: 0.3, maxTokens: 1000 });
api.log(`Heavy response: ${result.content}`);
} catch (error) {
// notification already shown to the user
api.log.error(`Heavy model not available: ${error.message}`);
}let full = '';
for await (const chunk of api.llm.stream.chat([
{ role: 'user', content: 'Tell me a short joke' },
])) {
if (chunk.content) full += chunk.content;
if (chunk.done) api.log(`Stream complete: ${full}`);
}The agent
Register tools, shape the system prompt, and evolve prompt skills over time.
Agent harness
api.agentagentA turn-by-turn tool-calling engine between user messages and the LLM.
When the harness is active it intercepts each outbound message, runs a structured loop — call tools, feed results back to the LLM — and delivers only the final assistant response to the chat. It is transparent when idle: with no tools registered and an unmodified prompt, isActive() is false and chat streams exactly as before, with zero overhead.
The loop
- The user sends a message in the chat view.
- The harness builds the prompt: composed system prompt + tool definitions + history.
- It calls the LLM. If the response contains <tool_call> blocks, each tool runs and results are appended as <tool_result> blocks.
- The updated conversation goes back to the LLM and the loop repeats.
- Once the LLM responds with no tool calls, that text becomes the final assistant message.
- If maxTurns is hit first, the harness reports an error and stops.
Tools
api.agent.registerTool(tool)→ () => voidapi.agent.unregisterTool(name)api.agent.listTools()api.agent.hasTool(name)interface ToolDefinition {
name: string; // globally unique, kebab-case
description: string; // shown to the LLM
parameters: {
type: 'object';
properties: Record<string, ToolParameter>;
required?: string[];
};
annotations?: ToolAnnotations; // readOnlyHint, destructiveHint, …
timeoutMs?: number;
handler: (
args: Record<string, unknown>,
context?: ToolHandlerContext, // { signal, onProgress }
) => ToolResult | Promise<ToolResult>;
}const unregister = api.agent.registerTool({
name: 'fetch-url',
description: 'Fetches the plain-text content of a URL.',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'Full URL including https://' },
},
required: ['url'],
},
annotations: { readOnlyHint: true, idempotentHint: true },
handler: async (args) => {
const res = await fetch(args.url as string);
if (!res.ok) return { content: `HTTP ${res.status}`, isError: true };
return { content: (await res.text()).slice(0, 4000) };
},
});Built-in tools
Six read-only tools are always present: get-current-time, get-system-info, get-working-directory, get-theme-info, list-views, list-providers.
System prompt & profiles
api.agent.getSystemPrompt()api.agent.appendSystemPrompt(segment, id?)api.agent.prependSystemPrompt(segment, id?)api.agent.removeSystemPromptSegment(id)api.agent.setPromptProfile(profile)api.agent.setResponseContract(contract)api.agent.setPromptProfile({
guardrails: [
'Never claim a file was changed unless a tool result in this run confirms it.',
'If a tool fails, explain the blocker plainly instead of pretending it succeeded.',
],
finalAnswerGuidance: ['State what you verified before giving the conclusion.'],
});Config, state & events
api.agent.setConfig(config)api.agent.isActive()api.agent.onToolCall(listener)→ () => voidapi.agent.onToolResult(listener)→ () => voidagentToolCall{ threadId, call }Immediately before a tool handler runs.agentToolProgress{ call, chunk }Each incremental progress chunk.agentToolResult{ call, result }Immediately after a handler returns.agentTurnComplete{ turnNumber }After all tools in a turn finish.agentRunComplete{ totalTurns }When the harness returns the final response.Evolve
api.evolveagentRegister evolvable prompt skills and observe promotions.
Evolve treats some prompt segments as skills: frozen metadata plus a mutable markdown body. Extensions register skills they own; only the body changes through evolve runs — never the frontmatter or segment id. The agent permission gates every operation.
api.evolve.registerSkill(registration)→ () => voidapi.evolve.getSkillStatus(id)api.evolve.onSkillUpdated(listener)→ () => voidapi.evolve.getStatus()interface SkillRegistration {
id: string; // kebab-case, globally unique
name: string;
description: string; // doubles as the judge's rubric anchor
segmentId: string; // harness segment this body backs
position: 'prepend' | 'append';
defaultBody: string; // persisted as version 1
}const dispose = await api.evolve.registerSkill({
id: 'final-answer-style',
name: 'Final answer style',
description: 'Keep final answers concise and action-oriented.',
segmentId: 'vanish-agent-final-answer-style',
position: 'append',
defaultBody: [
'Style guidance for final answers:',
'- Lead with the answer in the first sentence.',
'- Avoid restating the question.',
].join('\n'),
});evolve:skillRegistered{ id }After a skill is registered.evolve:skillUpdated{ id, version, bodyHash }After a successful promotion or rollback.evolve:runFinished{ runId, skillId, result }After an evolve run completes.Interface surfaces
Themes, modals, settings, the command palette, views, and keyboard input.
Themes
api.getThemeRead and change the active theme.
Read the active theme or any registered theme, and switch between them. Custom themes your extension registers are automatically available here.
api.getTheme()→ Themeapi.getThemeByName(name)→ Theme | nullapi.setTheme(name)api.listThemes()→ string[]api.getThemes()→ Theme[]const current = api.getTheme();
api.log(`Current background: ${current.colors.background}`);
if (api.listThemes().includes('my-custom-theme')) {
api.setTheme('my-custom-theme');
}Modals
api.modalOpen and close stacking overlay modals.
Present overlay modals that block the main views until dismissed — alerts, confirmations, input prompts. Modals stack; opening a new one displays it on top.
api.modal.open<T>(modal, onClose?)api.modal.closeTop()api.modal.closeAll()api.modal.hasModals()import { BaseModal } from 'vanish-tui';
class AlertModal extends BaseModal<boolean> {
// ... modal implementation ...
}
const alert = new AlertModal('Warning', 'Are you sure?');
api.modal.open(alert, (result) => {
api.log(result ? 'User confirmed.' : 'User cancelled.');
});Settings
api.settingsRegister custom settings sections and control element visibility.
Inject your own settings sections into the built-in Settings view, or toggle the visibility of existing elements. Register a section with the same id as an existing one and the core merges your elements into it.
api.settings.registerSections(sections)→ () => voidapi.settings.getSections()api.settings.setElementVisibility(id, visible)api.settings.registerPresentation(presentation)→ () => voidconst cleanup = api.settings.registerSections([{
id: 'my-extension',
title: 'My Extension Config',
elements: [{
id: 'enable-feature-x',
type: 'boolean',
label: 'Enable Feature X',
value: 'Enabled',
editable: true,
sectionId: 'my-extension',
persistence: {
namespace: 'myExtension',
key: 'enabled',
fromStored: (s) => (s === true ? 'Enabled' : 'Disabled'),
toStored: (v) => v === 'Enabled',
},
onActivate: async () => ({ message: 'Feature X toggled.', tone: 'success' }),
}],
}]);Command palette
api.commandsRegister first-class palette commands and drive the palette surface.
The palette (default Ctrl+Shift+P) surfaces every invocable action. Settings, theme switches, and view navigation are auto-adopted, so it's populated on day one. Register your own commands to join the fuzzy-scored results, MRU history, pinning, and parameterized sub-pick flows.
api.commands.registerCommand(command)→ () => voidapi.commands.registerCommands(commands)→ () => voidapi.commands.registerCategory(category)→ () => voidapi.commands.open(options?)api.commands.run(id, args?)api.commands.list(options?)→ CommandSummary[]interface CommandContext {
sourceViewId: string | null;
query: string;
mode: '>' | '#' | '@' | '?' | ':' | '/' | null;
pickFrom: <T>(items: PickableItem<T>[], prompt: string) => Promise<T | null>;
promptFor: (options: PromptOptions) => Promise<string | null>;
confirm: (options: { message: string; confirmWord?: string }) => Promise<boolean>;
runCommand: (id: string, args?: Record<string, unknown>) => Promise<CommandResult>;
namespace: string;
}api.commands.registerCommand({
id: 'my-ext.pick-rule',
label: 'Disable rule…',
category: 'extensions',
handler: async (ctx) => {
const rule = await ctx.pickFrom(
[
{ id: 'no-console', label: 'no-console', value: 'no-console' },
{ id: 'no-debugger', label: 'no-debugger', value: 'no-debugger' },
],
'Disable which rule?',
);
if (!rule) return { message: 'Cancelled.', tone: 'info' };
return { message: `Disabled ${rule}.`, tone: 'success' };
},
});Views
api.viewsInspect, enable/disable, and reorder views.
Programmatically inspect, switch, enable/disable, and reorder views — for view managers, shortcut toggles, or dynamic UI based on user preference. Disabling the active view auto-switches to the first enabled one.
api.views.listRegistered()→ { id, name }[]api.views.getCurrent()api.views.activate(viewId)api.views.isEnabled(viewId)api.views.setEnabled(viewId, enabled)api.views.getOrder()→ string[]api.views.setOrder(order)// toggle a view
const chatEnabled = api.views.isEnabled('chat');
api.views.setEnabled('chat', !chatEnabled);
// reverse the sidebar order
api.views.setOrder([...api.views.getOrder()].reverse());
api.events.on('viewEnabledChanged', ({ viewId, enabled }) => {
api.log(`View ${viewId} was ${enabled ? 'enabled' : 'disabled'}`);
});Input
api.inputIntercept keys and drive view-aware navigation.
Intercept keyboard activity before it reaches the current view, and invoke the same navigation actions the core UI uses. Mouse activity comes through api.events, not here.
api.input.getContext()→ { currentViewId, mode, modalOpen }api.input.registerHandler(handler)api.input.navigate(command)api.input.nextView() / prevView()interface KeyPress {
name: string;
ctrl: boolean;
meta: boolean;
alt: boolean;
shift: boolean;
sequence: string; // raw terminal bytes
text?: string; // printable character, when reported
}api.input.registerHandler((key, context) => {
if (context.currentViewId !== 'chat' || context.modalOpen) return false;
if (key.ctrl && key.name === 'h') {
api.log('Open chat history modal');
return true;
}
return false;
});
api.events.on('mouse:scroll', (e) => {
if (e.scrollDelta < 0) api.log('Scrolled up');
});Platform & security
Location, encrypted secrets, permissions, the outbound-signal recorder, and extension management.
Location
api.locationlocationAccess secure, IP-derived user location.
Access the user's location, detected via IP-based geolocation and cached by the core. Restricted — requires explicit authorization under Security & Permissions.
api.location.get()→ Promise<{ lat, lon, city, region } | null>api.location.getSummary()→ string | nullconst location = await api.location.get();
if (location) {
api.log(`Detected: ${location.city}, ${location.region}`);
} else {
api.log('Location access denied or unavailable.');
}
const summary = api.location.getSummary();
if (summary) api.renderer.drawText(0, 0, summary);Secrets
api.secretssecretsEncrypted, namespaced key-value storage.
A secure, encrypted local store for API keys and tokens. Each extension gets a namespace-scoped helper for its own values; cross-extension access is an explicit opt-in via forNamespace.
api.secrets.namespaceapi.secrets.get(key)→ Promise<string | null>api.secrets.set(key, value)api.secrets.delete(key)api.secrets.has(key)api.secrets.forNamespace(namespace)const tokenKey = 'github-personal-access-token';
if (!(await api.secrets.has(tokenKey))) {
await api.secrets.set(tokenKey, 'ghp_example123');
api.log('Token securely stored!');
} else {
const token = await api.secrets.get(tokenKey);
api.log('Retrieved stored token securely.');
}vanish audit secrets # last 100 entries, all extensions
vanish audit secrets -x my_extension # filter to one extension
vanish audit secrets --json -n 500 # JSON output, last 500 entriesPermissions
api.permissionsIntrospect, request, and (when privileged) resolve permissions.
Each extension tracks four flags: secrets, llm, location, agent. Introspect what you have, request what you don't, and — with the permissionManager capability — resolve other extensions' pending requests.
Reading & requesting
api.permissions.getAll()→ { secrets, llm, location, agent }api.permissions.hasPermission(permission)api.permissions.check(required)→ { granted, denied }api.permissions.request(required, options?)→ PermissionRequestResultManaging requests
Read methods are open to everyone; mutating methods are gated on the manifest capability permissionManager (one owner per capability).
api.permissions.requests.list()→ PermissionRequest[]api.permissions.requests.onChanged(listener)→ () => voidapi.permissions.requests.resolve(ext, perm, granted)api.permissions.requests.resolveAll(granted)const { denied } = api.permissions.check(['agent']);
if (denied.includes('agent')) {
api.permissions.request(['agent'], {
title: 'Search Extension: Agent permission required',
message: 'Needs agent access to expose web search tools.',
duration: 0,
});
}{
"name": "notify_request_perms",
"capabilities": ["permissionManager"]
}Outguard
api.outguardInspect and report outbound extension signals.
Outguard is the outbound-signal recorder. Vanish automatically records extension-attributed fetch(...) calls plus calls through protected surfaces (LLM presets, location). It redacts common secret query params (token, api_key, secret, password, …) before writing a signal.
api.outguard.report(signal)api.outguard.getRecent(limit?)api.outguard.getRecentNotifications(limit?)api.outguard.report({
kind: 'custom',
target: 'vendor-sdk:lookup',
metadata: { lookupType: 'symbol', symbol: 'MSFT' },
});
const recent = await api.outguard.getRecent(10);
api.log(`Outguard has ${recent.length} recent signal(s).`);Extension management
api.extensionsInspect, enable/disable, and reload installed extensions.
Inspect the extensions discovered in the active directory and toggle them — useful for managers, compatibility checkers, and installer flows.
api.extensions.listInstalled()→ Promise<InstalledExtension[]>api.extensions.getInstalled(name)api.extensions.isEnabled(name)api.extensions.setEnabled(name, enabled)api.extensions.reload(){
"name": "vanish_hud_plus",
"displayName": "Vanish HUD Plus",
"version": "1.0.0",
"dependencies": ["vanish_hud"],
"main": "main.ts"
}async function safeDisable(api: VanishAPI, name: string) {
const ext = await api.extensions.getInstalled(name);
if (!ext) return;
if (ext.dependents.length > 0) {
api.notifications.send({
title: 'Dependents Warning',
message: `Disabling "${ext.displayName}" affects: ${ext.dependents.join(', ')}`,
tone: 'warning',
duration: 5000,
});
}
await api.extensions.setEnabled(name, false);
}