Getting Started
Extensions let you add new capabilities to Stina — from connecting AI providers to giving the assistant entirely new skills. This guide walks you through creating your first extension from an empty folder to a working build.
What is a Stina Extension?
Section titled “What is a Stina Extension?”A Stina extension is a standalone TypeScript project that builds against the @stina/extension-api package on npm. Extensions run inside Stina’s extension host and interact with the application through a well-defined API.
There are two types of extensions:
- Provider extensions connect Stina to AI models. Examples include the Ollama extension (local models) and the OpenAI extension (cloud API).
- Tool extensions add features that the AI can use during conversations, such as reading emails, managing todos, or remembering people.
Every extension has an activate function that Stina calls on startup, a manifest.json that declares what it contributes, and a build step that produces a single JavaScript bundle.
Prerequisites
Section titled “Prerequisites”Before you start, make sure you have:
- Node.js 18 or later
- pnpm package manager
- Basic TypeScript knowledge
- A running Stina instance (desktop app, web, or from source) to test your extension
Project Setup
Section titled “Project Setup”Create a new directory and initialize it:
mkdir my-extension && cd my-extensionpnpm initpnpm add -D @stina/extension-api tsup typescriptThis installs the three dependencies you need:
| Package | Purpose |
|---|---|
@stina/extension-api | Type definitions and contracts for the extension API |
tsup | Fast TypeScript bundler built on esbuild |
typescript | TypeScript compiler for type checking |
Extension Structure
Section titled “Extension Structure”A minimal extension looks like this:
my-extension/ src/ index.ts # Entry point (activate/deactivate) manifest.json # Extension metadata and declarations tsup.config.ts # Build configuration package.jsonThe following sections walk through each file.
Create manifest.json
Section titled “Create manifest.json”The manifest tells Stina everything it needs to know about your extension — its identity, what permissions it needs, and what it contributes.
Create manifest.json in the project root:
{ "id": "my-extension", "name": "My Extension", "version": "1.0.0", "description": "A simple Stina extension", "author": { "name": "Your Name" }, "type": "tools", "main": "index.js", "permissions": ["tools.register"], "engines": { "stina": ">=0.5.0" }, "contributes": { "tools": [ { "id": "hello", "name": "Hello World", "description": "A simple greeting tool" } ] }}Key fields:
id— A unique identifier for your extension. Use lowercase with hyphens.type— Either"tools"or"provider".main— The built entry point file (relative to the build output).permissions— The APIs your extension needs access to. Stina only exposes the APIs you declare here.contributes.tools— Declares the tools your extension will register. Each tool listed here must be registered in code with a matchingid.
Create src/index.ts
Section titled “Create src/index.ts”This is your extension’s entry point. Stina calls the activate function when the extension loads.
Create src/index.ts:
import type { ExtensionModule, ExtensionContext } from '@stina/extension-api'
const extension: ExtensionModule = { activate(context: ExtensionContext) { context.tools?.register({ id: 'hello', name: 'Hello World', description: 'A simple greeting tool', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Name to greet' } } }, async execute(params) { const name = (params.name as string) || 'World' return { success: true, data: `Hello, ${name}!` } } }) }}
export default extensionA few things to note:
activateis called once when Stina loads the extension. This is where you register all your tools, providers, and actions.context.toolsis only available if you declared thetools.registerpermission in the manifest. It will beundefinedotherwise, which is why the optional chaining (?.) is used.parametersuses JSON Schema to describe the input the AI should provide when calling your tool.executereceives the parameters from the AI and returns aToolResultwithsuccess,data, and optionally amessageorerror. It also receives anExecutionContextas a second argument, which provides access to storage, secrets, and request metadata.- The
activatefunction can return aDisposable(with adispose()method) for cleanup. Alternatively, you can define an optionaldeactivatemethod on the extension module for teardown logic.
The execute function in detail
Section titled “The execute function in detail”The full signature of a tool’s execute method is:
async execute(params: Record<string, unknown>, context: ExecutionContext): Promise<ToolResult>The ExecutionContext gives you access to per-request information:
async execute(params, context) { // Access user-scoped storage const prefs = await context.userStorage.get('preferences', 'theme')
// Access extension-scoped secrets const apiKey = await context.secrets.get('api-key')
// Extension metadata console.log(`Running ${context.extension.id} v${context.extension.version}`)
return { success: true, data: 'Done' }}Build Configuration
Section titled “Build Configuration”Create tsup.config.ts in the project root:
import { defineConfig } from 'tsup'
export default defineConfig({ entry: ['src/index.ts'], format: ['esm'], dts: false, clean: true, external: ['@stina/extension-api']})The important part is external: ['@stina/extension-api'] — this tells the bundler not to include the extension API in your bundle. Stina provides it at runtime.
Then add build scripts to your package.json:
{ "scripts": { "build": "tsup", "dev": "tsup --watch" }}Build and Test
Section titled “Build and Test”Build your extension:
pnpm buildThis produces a dist/index.js file. To test it in Stina:
- Open Stina
- Go to Settings (gear icon)
- Navigate to Extensions
- Click Install from folder
- Select your extension’s project directory
Stina will read your manifest.json, load the built bundle, and call activate. Once loaded, the AI will be able to use your “Hello World” tool in conversations.
During development, use watch mode for faster iteration:
pnpm devThis rebuilds automatically whenever you save a file. You may need to reload the extension in Stina after each rebuild.
Next Steps
Section titled “Next Steps”Now that you have a working extension, explore the rest of the documentation to build more advanced functionality:
- Manifest Reference — All manifest fields explained in detail
- Extension API — Full API documentation for tools, providers, storage, and more
- Permissions — Available permissions and what they unlock
- UI Components — Build settings panels and custom UIs for your extension
- Publishing — Package and distribute your extension to other Stina users