Extension API
This page documents every API surface available to Stina extensions at runtime. All types are exported from the @stina/extension-api package.
Extension Module
Section titled “Extension Module”Every extension must default-export an ExtensionModule. Stina calls activate() when the extension loads and deactivate() when it is unloaded.
import type { ExtensionModule, ExtensionContext } from '@stina/extension-api'
const extension: ExtensionModule = { activate(context: ExtensionContext) { // Register tools, providers, actions, scheduled jobs, etc. // Optionally return a Disposable for cleanup. }, deactivate() { // Optional teardown logic. }}
export default extensionExtensionModule
Section titled “ExtensionModule”| Member | Signature | Description |
|---|---|---|
activate | (context: ExtensionContext) => void | Disposable | Promise<void | Disposable> | Called once when the extension is loaded. Use the context to register everything the extension contributes. |
deactivate | () => void | Promise<void> | (optional) Called when the extension is unloaded. Use this for final cleanup if you did not return a Disposable from activate. |
If activate() returns a Disposable, its dispose() method is called automatically when the extension is deactivated.
ExtensionContext
Section titled “ExtensionContext”The context object passed to activate() is the extension’s gateway to every Stina API. Each API property is only present if the extension has declared the corresponding permission in its manifest.
interface ExtensionContext { readonly extension: { id: string; version: string; storagePath: string } readonly log: LogAPI // Always available readonly network?: NetworkAPI // Requires network:* permission readonly settings?: SettingsAPI // Requires settings.register readonly providers?: ProvidersAPI // Requires provider.register readonly tools?: ToolsAPI // Requires tools.register readonly actions?: ActionsAPI // Requires actions.register readonly events?: EventsAPI // Requires events.emit readonly scheduler?: SchedulerAPI // Requires scheduler.register readonly user?: UserAPI // Requires user.profile.read readonly chat?: ChatAPI // Requires chat.message.write readonly storage?: StorageAPI // Requires storage.collections readonly secrets?: SecretsAPI // Requires secrets.manage readonly backgroundWorkers?: BackgroundWorkersAPI // Requires background.workers}| Property | Required permission | Purpose |
|---|---|---|
extension | (none) | Read-only metadata: id, version, and storagePath (writable directory on disk). |
log | (none) | Logging. Always available. |
network | network:* or network:<domain> | HTTP requests. |
settings | settings.register | Read/write extension settings. |
providers | provider.register | Register AI providers. |
tools | tools.register | Register tools the AI can call. |
actions | actions.register | Register backend handlers for UI components. |
events | events.emit | Emit events to trigger UI refreshes. |
scheduler | scheduler.register | Schedule one-time or recurring jobs. |
user | user.profile.read | Read the current user’s profile. |
chat | chat.message.write | Append instructions to conversations. |
storage | storage.collections | Document storage (activation-time only). |
secrets | secrets.manage | Encrypted key-value storage (activation-time only). |
backgroundWorkers | background.workers | Long-running background tasks. |
LogAPI
Section titled “LogAPI”Logging is always available, no permission required. Log output appears in Stina’s extension log viewer.
interface LogAPI { debug(message: string, data?: Record<string, unknown>): void info(message: string, data?: Record<string, unknown>): void warn(message: string, data?: Record<string, unknown>): void error(message: string, data?: Record<string, unknown>): void}All four methods accept an optional data object that is attached as structured metadata alongside the message.
Example:
context.log.info('Extension activated')context.log.debug('Loaded config', { accountCount: 3 })context.log.error('Connection failed', { host: 'imap.example.com', code: 'ETIMEDOUT' })NetworkAPI
Section titled “NetworkAPI”Permission: network:* (all hosts) or network:<domain> (specific host).
All HTTP traffic goes through Stina’s network layer, which enforces the domain restrictions declared in the manifest.
interface NetworkAPI { fetch(url: string, options?: RequestInit): Promise<Response> fetchStream(url: string, options?: RequestInit): AsyncGenerator<string, void, unknown>}| Method | Description |
|---|---|
fetch | Standard fetch. Returns a Response identical to the Web Fetch API. |
fetchStream | Streaming fetch for NDJSON, SSE, or chunked responses. Yields text chunks as they arrive. |
Example — standard request:
const response = await context.network!.fetch('https://api.example.com/data', { headers: { 'Authorization': 'Bearer token' }})const data = await response.json()Example — streaming:
let buffer = ''for await (const chunk of context.network!.fetchStream(url, { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }})) { buffer += chunk // Process partial results as they arrive}fetchStream throws an Error if the request fails or encounters a network error.
ProvidersAPI
Section titled “ProvidersAPI”Permission: provider.register
Register an AI provider that Stina can use for chat completions, model listing, and optionally embeddings.
interface ProvidersAPI { register(provider: AIProvider): Disposable}AIProvider
Section titled “AIProvider”interface AIProvider { id: string name: string getModels(options?: GetModelsOptions): Promise<ModelInfo[]> chat(messages: ChatMessage[], options: ChatOptions): AsyncGenerator<StreamEvent, void, unknown> embed?(texts: string[]): Promise<number[][]>}| Member | Description |
|---|---|
id | Must match the provider ID declared in the manifest. |
name | Display name shown to the user. |
getModels | Return a list of models available from this provider. |
chat | Stream a chat completion. Must yield StreamEvent objects. |
embed | (optional) Generate vector embeddings for a list of texts. |
Supporting types
Section titled “Supporting types”ModelInfo — describes a single model:
interface ModelInfo { id: string name: string description?: string contextLength?: number}ChatMessage — a message in the conversation:
interface ChatMessage { role: 'user' | 'assistant' | 'system' | 'tool' content: string tool_calls?: ToolCall[] // Present on assistant messages when the model calls tools tool_call_id?: string // Present on tool messages as the response to a tool call}ToolCall — a tool invocation requested by the model:
interface ToolCall { id: string name: string arguments: Record<string, unknown>}ChatOptions — controls for a chat request:
interface ChatOptions { model?: string temperature?: number // 0 to 1 maxTokens?: number signal?: AbortSignal // For cancellation settings?: Record<string, unknown> // Provider-specific settings from model configuration tools?: ToolDefinition[] // Tools available for this request}GetModelsOptions — options passed to getModels:
interface GetModelsOptions { settings?: Record<string, unknown> // Provider-specific settings (e.g., URL for Ollama)}StreamEvent — events yielded during streaming:
type StreamEvent = | { type: 'content'; text: string } | { type: 'thinking'; text: string } | { type: 'tool_start'; name: string; input: unknown; toolCallId: string } | { type: 'tool_end'; name: string; output: unknown; toolCallId: string } | { type: 'done'; usage?: { inputTokens: number; outputTokens: number } } | { type: 'error'; message: string }| Event | When it is yielded |
|---|---|
content | The model produced text output. |
thinking | The model produced reasoning/thinking text (chain-of-thought). |
tool_start | The model is calling a tool. |
tool_end | A tool call has completed. |
done | Generation is finished. Token usage may be included. |
error | An error occurred during generation. |
Example — registering a provider:
context.providers?.register({ id: 'my-provider', name: 'My AI Provider',
async getModels() { return [{ id: 'model-1', name: 'Model One', contextLength: 8192 }] },
async *chat(messages, options) { const response = await callExternalAPI(messages, options) for await (const token of response.stream) { yield { type: 'content', text: token } } yield { type: 'done', usage: { inputTokens: 100, outputTokens: 50 } } }})ToolsAPI
Section titled “ToolsAPI”Permission: tools.register
Register tools that the AI assistant can call during conversations. Each tool must have a matching entry in contributes.tools in the manifest.
interface ToolsAPI { register(tool: Tool): Disposable}interface Tool { id: string name: string description: string parameters?: Record<string, unknown> // JSON Schema execute(params: Record<string, unknown>, context: ExecutionContext): Promise<ToolResult>}| Member | Description |
|---|---|
id | Must match the tool ID in the manifest’s contributes.tools. |
name | Display name for the tool. |
description | Description that helps the AI understand when and how to use the tool. |
parameters | A JSON Schema object describing the expected input. |
execute | Called when the AI invokes the tool. Receives parameters and an ExecutionContext. |
ToolResult
Section titled “ToolResult”interface ToolResult { success: boolean data?: unknown // Structured result data the AI can interpret message?: string // Human-readable summary error?: string // Error description if success is false}Example:
context.tools?.register({ id: 'search-contacts', name: 'Search Contacts', description: 'Search the user\'s contact list by name or email', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search term' }, limit: { type: 'number', description: 'Max results', default: 10 } }, required: ['query'] }, async execute(params, context) { const contacts = await context.userStorage.find('contacts', { name: { $contains: params.query as string } }, { limit: (params.limit as number) || 10 })
return { success: true, data: contacts, message: `Found ${contacts.length} contacts` } }})ActionsAPI
Section titled “ActionsAPI”Permission: actions.register
Register backend handlers that UI components (buttons, forms, settings views) can invoke. Unlike tools, actions are triggered by user interactions in the UI, not by the AI.
interface ActionsAPI { register(action: Action): Disposable}Action
Section titled “Action”interface Action { id: string execute(params: Record<string, unknown>, context: ExecutionContext): Promise<ActionResult>}| Member | Description |
|---|---|
id | Unique action ID within the extension. Referenced from UI component definitions. |
execute | Called when the UI triggers the action. Parameters come from the component, with $-prefixed dynamic values already resolved. |
ActionResult
Section titled “ActionResult”interface ActionResult { success: boolean data?: unknown // Data returned to the UI error?: string // Error message if success is false}Example:
context.actions?.register({ id: 'save-account', async execute(params, context) { const { email, password } = params as { email: string; password: string } await context.userSecrets.set(`account-${email}`, password) await context.userStorage.put('accounts', email, { email, enabled: true }) return { success: true, data: { email } } }})ExecutionContext
Section titled “ExecutionContext”The ExecutionContext is passed to every execute() call in tools, actions, and scheduler callbacks. It provides request-scoped access to storage, secrets, and metadata. Using per-request context (instead of global state) eliminates race conditions and makes multi-user setups safe.
interface ExecutionContext { readonly userId?: string readonly extension: { id: string; version: string; storagePath: string } readonly storage: StorageAPI readonly userStorage: StorageAPI readonly secrets: SecretsAPI readonly userSecrets: SecretsAPI}| Property | Description |
|---|---|
userId | The ID of the user who triggered this request. Always defined for tool calls, action calls, and scheduler callbacks. Undefined only during activation-time operations. |
extension | Read-only extension metadata. |
storage | Extension-scoped document storage, shared across all users. |
userStorage | User-scoped document storage, isolated per user. |
secrets | Extension-scoped encrypted secrets, shared across all users. |
userSecrets | User-scoped encrypted secrets, isolated per user. |
In a single-user desktop setup, storage and userStorage behave the same way. In multi-user (server) deployments, userStorage keeps each user’s data separate.
Example:
async execute(params, context) { // Extension-wide config (same for everyone) const config = await context.storage.get('settings', 'api-config')
// Per-user preferences const prefs = await context.userStorage.get('preferences', 'display')
// Extension-wide secret const sharedKey = await context.secrets.get('shared-api-key')
// Per-user secret const userToken = await context.userSecrets.get('oauth-token')
return { success: true, data: { configured: !!config } }}SettingsAPI
Section titled “SettingsAPI”Permission: settings.register
Read and write extension settings. Settings declared in contributes.settings in the manifest are automatically rendered in Stina’s settings UI.
interface SettingsAPI { getAll<T extends Record<string, unknown>>(): Promise<T> get<T>(key: string): Promise<T | undefined> set(key: string, value: unknown): Promise<void> onChange(callback: (key: string, value: unknown) => void): Disposable}| Method | Description |
|---|---|
getAll() | Returns all settings for this extension as a typed object. |
get(key) | Returns the value of a single setting, or undefined if not set. |
set(key, value) | Updates a setting value. |
onChange(callback) | Subscribes to setting changes. The callback receives the changed key and its new value. Returns a Disposable to unsubscribe. |
Example:
const settings = await context.settings!.getAll<{ apiUrl: string; timeout: number }>()context.log.info('Current API URL', { url: settings.apiUrl })
// React to setting changescontext.settings!.onChange((key, value) => { if (key === 'apiUrl') { reconnect(value as string) }})StorageAPI
Section titled “StorageAPI”Permission: storage.collections
MongoDB-inspired document storage. All operations are automatically scoped to the calling extension. Data is stored as JSON documents organized into collections.
Collections can be declared in the manifest under contributes.storage.collections with optional indexes for faster queries.
interface StorageAPI { // Single document operations put<T extends object>(collection: string, id: string, data: T): Promise<void> get<T>(collection: string, id: string): Promise<T | undefined> delete(collection: string, id: string): Promise<boolean>
// Query operations find<T>(collection: string, query?: Query, options?: QueryOptions): Promise<T[]> findOne<T>(collection: string, query: Query): Promise<T | undefined> count(collection: string, query?: Query): Promise<number>
// Bulk operations putMany<T extends object>(collection: string, docs: Array<{ id: string; data: T }>): Promise<void> deleteMany(collection: string, query: Query): Promise<number>
// Collection management dropCollection(collection: string): Promise<void> listCollections(): Promise<string[]>}Single document operations
Section titled “Single document operations”| Method | Description |
|---|---|
put(collection, id, data) | Store or replace a document. |
get(collection, id) | Retrieve a document by ID. Returns undefined if not found. |
delete(collection, id) | Delete a document. Returns true if it existed. |
Query operations
Section titled “Query operations”| Method | Description |
|---|---|
find(collection, query?, options?) | Find all documents matching a query. Returns an empty array if none match. |
findOne(collection, query) | Find the first document matching a query. Returns undefined if none match. |
count(collection, query?) | Count documents matching a query. |
Bulk operations
Section titled “Bulk operations”| Method | Description |
|---|---|
putMany(collection, docs) | Store multiple documents in a single operation. More efficient than calling put in a loop. |
deleteMany(collection, query) | Delete all documents matching a query. Returns the count of deleted documents. |
Collection management
Section titled “Collection management”| Method | Description |
|---|---|
dropCollection(collection) | Delete an entire collection and all its documents. This cannot be undone. |
listCollections() | List all collection names owned by this extension. |
Query syntax
Section titled “Query syntax”The Query object supports exact matching and comparison operators:
interface Query { [field: string]: | unknown // Exact match | { $gt?: unknown } // Greater than | { $gte?: unknown } // Greater than or equal | { $lt?: unknown } // Less than | { $lte?: unknown } // Less than or equal | { $ne?: unknown } // Not equal | { $in?: unknown[] } // In array | { $contains?: string } // String contains (case-insensitive)}QueryOptions
Section titled “QueryOptions”interface QueryOptions { sort?: { [field: string]: 'asc' | 'desc' } limit?: number offset?: number}Examples:
// Store a documentawait storage.put('users', 'user-123', { name: 'Alice', role: 'admin' })
// Retrieve by IDconst user = await storage.get<User>('users', 'user-123')
// Find with exact matchconst admins = await storage.find<User>('users', { role: 'admin' })
// Find with comparison operatorsconst recent = await storage.find<Event>('events', { createdAt: { $gt: '2024-01-01' }}, { sort: { createdAt: 'desc' }, limit: 20 })
// Case-insensitive text searchconst matches = await storage.find<Contact>('contacts', { name: { $contains: 'alice' }})
// Count documentsconst total = await storage.count('users')const activeCount = await storage.count('users', { status: 'active' })
// Bulk insertawait storage.putMany('logs', [ { id: 'log-1', data: { level: 'info', message: 'Started' } }, { id: 'log-2', data: { level: 'info', message: 'Connected' } }])
// Bulk deleteconst removed = await storage.deleteMany('logs', { level: 'debug' })SecretsAPI
Section titled “SecretsAPI”Permission: secrets.manage
Encrypted key-value storage for sensitive data such as API keys, tokens, and passwords. Values are encrypted at rest. All operations are scoped to the calling extension.
interface SecretsAPI { set(key: string, value: string): Promise<void> get(key: string): Promise<string | undefined> delete(key: string): Promise<boolean> list(): Promise<string[]>}| Method | Description |
|---|---|
set(key, value) | Store or replace a secret. |
get(key) | Retrieve a secret value. Returns undefined if not found. |
delete(key) | Delete a secret. Returns true if it existed. |
list() | List all secret keys (not values) owned by this extension. |
Example:
// Store a secret during setupawait context.secrets!.set('api-key', 'sk-abc123')
// Read it back later (e.g., in a tool execute)const key = await context.secrets.get('api-key')if (!key) { return { success: false, error: 'API key not configured' }}SchedulerAPI
Section titled “SchedulerAPI”Permission: scheduler.register
Schedule one-time or recurring jobs. When a job fires, the registered callback is invoked with a payload and an ExecutionContext.
interface SchedulerAPI { schedule(job: SchedulerJobRequest): Promise<void> cancel(jobId: string): Promise<void> onFire(callback: (payload: SchedulerFirePayload, context: ExecutionContext) => void | Promise<void>): Disposable}| Method | Description |
|---|---|
schedule(job) | Register a new scheduled job. |
cancel(jobId) | Cancel a previously scheduled job by its ID. |
onFire(callback) | Register a handler that runs when any of this extension’s jobs fire. |
SchedulerJobRequest
Section titled “SchedulerJobRequest”interface SchedulerJobRequest { id: string schedule: SchedulerSchedule payload?: Record<string, unknown> misfire?: 'run_once' | 'skip' userId: string}| Field | Description |
|---|---|
id | Unique job identifier. |
schedule | When the job should fire (see below). |
payload | Arbitrary data passed to the callback when the job fires. |
misfire | What to do if the job was missed (e.g., the app was closed): 'run_once' fires it once on next startup, 'skip' ignores the missed run. |
userId | The user this job belongs to. Passed through to the callback’s ExecutionContext. |
Schedule types
Section titled “Schedule types”type SchedulerSchedule = | { type: 'at'; at: string } // One-time | { type: 'cron'; cron: string; timezone?: string } // Cron | { type: 'interval'; everyMs: number } // Fixed interval| Type | Description | Example |
|---|---|---|
at | Fire once at a specific ISO 8601 date/time. | { type: 'at', at: '2024-12-25T10:00:00Z' } |
cron | Fire on a cron schedule. Optional timezone (defaults to UTC). | { type: 'cron', cron: '0 */6 * * *', timezone: 'Europe/Stockholm' } |
interval | Fire at a fixed interval in milliseconds. | { type: 'interval', everyMs: 300_000 } (every 5 minutes) |
SchedulerFirePayload
Section titled “SchedulerFirePayload”interface SchedulerFirePayload { id: string payload?: Record<string, unknown> scheduledFor: string // When the job was supposed to fire firedAt: string // When it actually fired delayMs: number // Difference between scheduledFor and firedAt userId: string}Example:
// Register the fire handlercontext.scheduler?.onFire(async (fire, execContext) => { context.log.info('Job fired', { jobId: fire.id, delay: fire.delayMs })
if (fire.id === 'check-mail') { const accounts = await execContext.userStorage.find('accounts', { enabled: true }) for (const account of accounts) { await checkMail(account, execContext) } }})
// Schedule a recurring jobawait context.scheduler?.schedule({ id: 'check-mail', userId: 'user-1', schedule: { type: 'interval', everyMs: 5 * 60 * 1000 }, misfire: 'run_once'})
// Schedule a one-time reminderawait context.scheduler?.schedule({ id: 'reminder-abc', userId: 'user-1', schedule: { type: 'at', at: '2024-12-25T10:00:00Z' }, payload: { message: 'Merry Christmas!' }})BackgroundWorkersAPI
Section titled “BackgroundWorkersAPI”Permission: background.workers
Run long-lived background tasks that survive individual requests. Tasks can be automatically restarted on failure with configurable backoff.
interface BackgroundWorkersAPI { start(config: BackgroundTaskConfig, callback: BackgroundTaskCallback): Promise<Disposable> stop(taskId: string): Promise<void> getStatus(): Promise<BackgroundTaskHealth[]>}| Method | Description |
|---|---|
start(config, callback) | Start a new background task. Returns a Disposable that stops the task when disposed. |
stop(taskId) | Stop a running task by its ID. |
getStatus() | Get health status of all background tasks for this extension. |
BackgroundTaskConfig
Section titled “BackgroundTaskConfig”interface BackgroundTaskConfig { id: string name: string userId: string restartPolicy: BackgroundRestartPolicy payload?: Record<string, unknown>}| Field | Description |
|---|---|
id | Unique task ID within the extension. |
name | Human-readable name for logging and observability. |
userId | User that owns this task. |
restartPolicy | Controls automatic restart behavior on failure. |
payload | Optional data passed to the callback. |
BackgroundRestartPolicy
Section titled “BackgroundRestartPolicy”interface BackgroundRestartPolicy { type: 'always' | 'on-failure' | 'never' maxRestarts?: number // 0 = unlimited (default: 0) initialDelayMs?: number // Default: 1000 maxDelayMs?: number // Default: 60000 backoffMultiplier?: number // Default: 2}type value | Behavior |
|---|---|
'always' | Restart the task regardless of how it exited. |
'on-failure' | Restart only if the task threw an error. |
'never' | Never restart automatically. |
Restarts use exponential backoff: the delay starts at initialDelayMs and multiplies by backoffMultiplier after each restart, up to maxDelayMs.
BackgroundTaskContext
Section titled “BackgroundTaskContext”The callback receives a BackgroundTaskContext that extends ExecutionContext with task-specific features:
interface BackgroundTaskContext extends ExecutionContext { readonly signal: AbortSignal reportHealth(status: string): void readonly log: LogAPI}| Property | Description |
|---|---|
signal | An AbortSignal that is triggered when the task should stop. Your callback should check this signal regularly and exit gracefully. |
reportHealth(status) | Report the task’s current status for observability. |
log | Logger tagged with the task ID. |
BackgroundTaskHealth
Section titled “BackgroundTaskHealth”interface BackgroundTaskHealth { taskId: string name: string userId: string status: 'pending' | 'running' | 'stopped' | 'failed' | 'restarting' restartCount: number lastHealthStatus?: string lastHealthTime?: string error?: string}Example:
const task = await context.backgroundWorkers!.start({ id: 'imap-idle', name: 'IMAP IDLE listener', userId: 'user-1', restartPolicy: { type: 'on-failure', maxRestarts: 10, initialDelayMs: 2000 }}, async (ctx) => { const connection = await connectIMAP(ctx) try { while (!ctx.signal.aborted) { ctx.reportHealth('Waiting for new mail...') const mail = await connection.idle({ signal: ctx.signal }) if (mail) { await processMail(mail, ctx) ctx.reportHealth(`Processed mail: ${mail.subject}`) } } } finally { await connection.close() }})
// Later, stop the tasktask.dispose()
// Or check statusconst statuses = await context.backgroundWorkers!.getStatus()for (const s of statuses) { context.log.info(`Task ${s.name}: ${s.status}, restarts: ${s.restartCount}`)}EventsAPI
Section titled “EventsAPI”Permission: events.emit
Emit named events that can trigger UI refreshes. UI components and panels can declare a refreshOn property in the manifest to automatically re-render when a specific event is emitted.
interface EventsAPI { emit(name: string, payload?: Record<string, unknown>): Promise<void>}| Parameter | Description |
|---|---|
name | The event name. UI components reference this in their refreshOn field. |
payload | Optional data attached to the event. |
Example:
// After modifying data, tell the UI to refreshawait context.events!.emit('contacts-updated', { count: newContacts.length })
// A panel in the manifest can then use:// "refreshOn": ["contacts-updated"]ChatAPI
Section titled “ChatAPI”Permission: chat.message.write
Append system-level instructions to the current conversation. This is used by extensions that need to inject context or guidance into the AI’s prompt.
interface ChatAPI { appendInstruction(message: ChatInstructionMessage): Promise<void>}ChatInstructionMessage
Section titled “ChatInstructionMessage”interface ChatInstructionMessage { text: string conversationId?: string userId?: string}| Field | Description |
|---|---|
text | The instruction text to inject into the conversation. |
conversationId | Target a specific conversation. If omitted, targets the current active conversation. |
userId | Target a specific user’s conversation. If omitted, uses the current user. |
Example:
await context.chat!.appendInstruction({ text: 'The user has 3 unread emails from their manager. Mention this if relevant.'})UserAPI
Section titled “UserAPI”Permission: user.profile.read
Read the current user’s profile information. Useful for personalizing tool behavior (e.g., formatting dates for the user’s timezone, addressing them by name).
interface UserAPI { getProfile(): Promise<UserProfile>}UserProfile
Section titled “UserProfile”interface UserProfile { firstName?: string nickname?: string language?: string // ISO 639-1 code, e.g., "sv", "en" timezone?: string // IANA timezone, e.g., "Europe/Stockholm"}Example:
const profile = await context.user!.getProfile()const greeting = profile.nickname ? `Hello, ${profile.nickname}!` : `Hello!`const tz = profile.timezone || 'UTC'Disposable Pattern
Section titled “Disposable Pattern”Many API methods return a Disposable object:
interface Disposable { dispose(): void}Call dispose() to unregister or clean up the resource. This is the primary mechanism for teardown.
Returning from activate:
activate(context) { const toolDisposable = context.tools?.register(myTool) const settingsDisposable = context.settings?.onChange(handleChange)
// Return a composite disposable return { dispose() { toolDisposable?.dispose() settingsDisposable?.dispose() } }}If activate() returns a Disposable, Stina calls dispose() automatically when the extension is deactivated. Alternatively, use the deactivate() method on the extension module for cleanup logic.
LocalizedString
Section titled “LocalizedString”Tool names and descriptions can be localized using a LocalizedString:
type LocalizedString = string | Record<string, string>When a plain string is provided, it is used as-is. When an object is provided, Stina selects the appropriate translation based on the user’s language setting, falling back to en if the user’s language is not available.
Example:
context.tools?.register({ id: 'get-weather', name: { en: 'Get Weather', sv: 'Hamta vader', de: 'Wetter abrufen' }, description: { en: 'Fetch current weather for a location', sv: 'Hamta aktuellt vader for en plats' }, async execute(params) { // ... }})Permissions Reference
Section titled “Permissions Reference”Each API requires a specific permission declared in the manifest’s permissions array. Here is the complete list:
| Permission | Unlocks |
|---|---|
network:* | NetworkAPI (all hosts) |
network:<domain> | NetworkAPI (specific domain only) |
storage.collections | StorageAPI |
secrets.manage | SecretsAPI |
user.profile.read | UserAPI |
chat.message.write | ChatAPI |
provider.register | ProvidersAPI |
tools.register | ToolsAPI |
actions.register | ActionsAPI |
settings.register | SettingsAPI |
events.emit | EventsAPI |
scheduler.register | SchedulerAPI |
background.workers | BackgroundWorkersAPI |
Additional permissions exist for capabilities not covered in this API reference:
| Permission | Purpose |
|---|---|
commands.register | Register keyboard commands |
panels.register | Register UI panels |
chat.history.read | Read conversation history |
chat.current.read | Read the current conversation |
user.location.read | Read user location data |
files.read | Read files on disk |
files.write | Write files on disk |
clipboard.read | Read clipboard contents |
clipboard.write | Write to clipboard |
Only request the permissions your extension actually needs. Users can see the full list of requested permissions before installing an extension.