Antinote supports custom JavaScript (ES6) extensions that let you add text to your note, modify your note, or open a URL/URI. Extensions can work locally or make network requests to external APIs. This guide walks you through creating an extension from scratch.
Building Specific Types of Extensions:
- 🌐 Network Extensions - Call external APIs safely and securely
- 🤖 AI & LLM Extensions - Build AI-powered extensions using the centralized AI service
- 🧮 Math Expression Evaluator - Use the built-in math evaluator in your extensions
- 🔒 Security & Privacy Guide - Understanding security architecture and best practices
Reference:
- 📖 Official Extensions Index - Complete catalog of all official extensions
An extension is a container for one or more commands, implemented as JavaScript file(s). Each extension has:
- A name (e.g., "random", "openai", "math")
- A version (e.g., "1.0.0")
- Author information (GitHub username recommended)
- A category for organization (e.g., "AI", "Math", "Date & Time")
- A data scope defining what note content the extension can access
- One or more commands
- Optional preferences for user configuration
- Optional API endpoints and required API keys for network extensions
Extensions can be implemented as:
Single-File (Simple):
my_extension/
index.js
extension.json
Multi-File (For Larger Extensions):
my_extension/
index.js (entry point)
helpers/ (shared utilities)
extractor.js
formatters.js
commands/ (command implementations)
csv.js
sort.js
extension.json (with "files" array)
Multi-file extensions:
- Must list all files in
extension.jsonunder"files"array index.jsmust always be first- Files are loaded sequentially in order
- Total size is calculated automatically
See the Multi-File Extensions section below for details.
By default, extensions are stored in:
~/Library/Application Support/Antinote/Extensions
You can customize this location in Preferences > Extensions > Choose Folder.
To open the default folder from Terminal:
open ~/Library/Application\ Support/Antinote/ExtensionsTo use an extension:
- Download the extension's JavaScript file
- Place it in the Extensions folder (or your custom folder)
- Go to Preferences > Extensions and click Reload Extensions
Then you can use any command from the extension inside Antinote using the syntax:
::command_name(arg1, arg2, ...)
For example, to generate a random number between 10 and 20:
::random_number(10, 20)
Press Tab or Enter to execute the command.
Note: Antinote Extensions uses JavaScriptCore, which supports full ES6 (ECMAScript 2015) features on macOS 14+. You can use modern JavaScript including const/let, arrow functions, template literals, destructuring, spread operators, and more.
Wrap your extension in an IIFE (Immediately Invoked Function Expression) to avoid variable conflicts:
(function() {
var extensionName = "my_extension";
// Create the extension root
var extensionRoot = new Extension({
name: extensionName, // Name of your extension
version: "1.0.0", // Version
endpoints: [], // API endpoints (if making network requests)
requiredAPIKeys: [], // Required API key IDs
author: "your_github", // Author (GitHub username)
category: "Utilities", // Category
dataScope: "none", // Data scope: "none", "line", or "full"
dependencies: [], // Dependencies (optional - other extensions this depends on)
isService: false // isService (optional - true if this provides services to other extensions)
});
// Define commands here...
})();Extension({name, version, endpoints, requiredAPIKeys, author, category, dataScope, dependencies, isService})
name(string): Unique name for your extensionversion(string): Version number (e.g., "1.0.0")endpoints(array): URLs your extension calls (e.g.,["https://api.example.com"])requiredAPIKeys(array): API key IDs needed (e.g.,["apikey_openai"])author(string): Your GitHub username or namecategory(string): Category for organization (e.g., "AI", "Math", "Date & Time", "Utilities")dataScope(string): Data access level - see Data Privacy belowdependencies(array, optional): Other extension names this depends on (e.g.,["ai_providers"])isService(boolean, optional): Set totrueif this extension provides services for other extensions (default:false)
Extensions must declare what note content they need access to. This is shown to users in the Extensions settings.
-
"none"- Extension receives NO note content (most private)- Use for: Date/time generators, calculators, random generators
- Example:
::today(),::tomorrow(),::random_number(1, 100)
-
"line"- Extension receives only the current line where command is used- Use for: Single-line processors, current context operations
- Example: AI commands that process current line only
-
"full"- Extension receives entire note content- Use for: Global search/replace, full-text analysis
⚠️ Users will see this requires full note access
Important: Choose the minimum scope needed. Users can see the data scope in Preferences > Extensions.
📖 Read full Security & Privacy Guide - Understanding security architecture, API key management, and best practices
Each command within an extension can have:
- A name (snake_case recommended)
- Parameters with types, names, help text, and defaults
- A type that defines how it modifies the note
- Tutorial examples showing usage
- An execute function with your logic
var my_command = new Command({
name: "my_command", // Command name (what user types)
// Parameters array
parameters: [
new Parameter({type: "float", name: "from", helpText: "Bottom range", default: 0}),
new Parameter({type: "float", name: "to", helpText: "Top range", default: 100}),
new Parameter({type: "bool", name: "int", helpText: "Round to nearest whole number", default: true})
],
type: "replaceLine", // Type: "insert", "replaceLine", "replaceAll", "openURL"
helpText: "Generate a random number between two values.", // Help text
// Tutorial examples
tutorials: [
new TutorialCommand({command: "my_command", description: "Generate a random number from 0 to 100"}),
new TutorialCommand({command: "my_command(10, 20)", description: "Generate from 10 to 20"}),
new TutorialCommand({command: "my_command(10, 20, false)", description: "Generate decimal from 10 to 20"})
],
extension: extensionRoot // Parent extension
});"insert"- Insert text after the command"replaceLine"- Replace the entire line with result"replaceAll"- Replace all note text with result"openURL"- Open a URL/URI in the default browser
new Parameter({type, name, helpText, default, required})Parameter Properties:
type(string): Parameter type - see belowname(string): Parameter name (shown to user)helpText(string): Description of the parameterdefault(any): Default value (for autocomplete/tutorials)required(boolean): Whether the parameter must be provided by the user (defaults totrue)
Parameter Types:
"float"- Decimal numbers"int"- Whole numbers"bool"- true/false (accepts: true, false, "true", "false")"string"- Text (use quotes if contains commas:"hello, world")"paragraph"- Multi-line text editor (shown as textarea in UI)"expression"/"mathExpression"- Math expressions evaluated via MathEvaluator
Required vs Optional Parameters:
Parameters are required by default. Set required: false to make a parameter optional:
parameters: [
// Required - user must provide this value
new Parameter({type: "string", name: "text", helpText: "Text to search", default: "TODO", required: true}),
// Optional - uses default if not provided
new Parameter({type: "bool", name: "caseSensitive", helpText: "Case sensitive", default: false, required: false})
]The default value serves two purposes:
- For autocomplete/tutorials - Shows users example values
- For optional parameters - Used when user doesn't provide a value
Define the command behavior by assigning an execute function:
my_command.execute = function(payload) {
// Parse parameters automatically (respects types and defaults)
var [from, to, int] = this.getParsedParams(payload);
// Validate inputs
if (from > to) {
return new ReturnObject({status: "error", message: "The 'from' value cannot be greater than 'to'."});
}
// Execute logic
var result = Math.random() * (to - from) + from;
if (int) {
result = Math.floor(result);
}
// Return result
return new ReturnObject({status: "success", message: "Random number generated.", payload: result});
};Your function receives a payload object:
{
parameters: ["10", "20", "true"], // Raw parameter values
fullText: "...", // Note content (based on dataScope)
userSettings: {
decimalSeparator: ".",
thousandsSeparator: ","
}
}Note: fullText content depends on your extension's dataScope:
"none"→fullTextis empty string"""line"→fullTextis current line only"full"→fullTextis entire note
new ReturnObject({status, message, payload})status:"success"or"error"message: User-friendly message shown in notificationpayload: Result string (for success) or empty (for error)
Extensions can make external API calls using the secure callAPI bridge function. Antinote's Swift bridge ensures API keys are never exposed to JavaScript code.
// 1. Declare endpoints and API keys
const extensionRoot = new Extension({
name: "weather",
version: "1.0.0",
endpoints: ["https://api.weatherapi.com/v1"], // Endpoints
requiredAPIKeys: ["apikey_weather"], // Required keys
author: "your_github",
category: "Utilities",
dataScope: "none"
});
// 2. Make API call
my_command.execute = function(payload) {
const url = `https://api.weatherapi.com/v1/current.json?key={{API_KEY}}&q=Boston`;
const result = callAPI("apikey_weather", url, "GET", JSON.stringify({}), "");
if (!result.success) {
return new ReturnObject({status: "error", message: `API failed: ${result.error}`});
}
const data = JSON.parse(result.data);
return new ReturnObject({status: "success", message: "Weather retrieved", payload: data.current.temp_f + "°F"});
};- API keys never exposed - Stored in macOS Keychain, accessed only by Swift
- Endpoint validation - Swift verifies all URLs match declared endpoints (prefix matching)
- Extension identity verification - Extensions cannot impersonate each other
{{API_KEY}}placeholder - Replaced by Swift with actual key value
📖 Read full Network Extensions Guide - Complete examples, API reference, patterns, and troubleshooting
Extensions can define user-configurable preferences that appear in Settings.
// After creating extensionRoot, register preferences
var modelPref = new Preference({
key: "model", // Key (used in code)
label: "OpenAI Model", // Label (shown to user)
type: "selectOne", // Type: "bool", "string", "selectOne", "selectMultiple"
defaultValue: "gpt-4o", // Default value
options: ["gpt-4o", "gpt-4o-mini"], // Options (for select types)
helpText: "Select which model to use" // Help text
});
extensionRoot.register_preference(modelPref);"bool"- Toggle on/off"string"- Free text input"paragraph"- Multi-line text editor (textarea)"selectOne"- Dropdown with single selection"selectMultiple"- Multiple checkboxes
my_command.execute = function(payload) {
// Get preference value
var model = getExtensionPreference("openai", "model") || "gpt-4o";
console.log("Using model:", model);
// ... use in your logic
};Antinote provides a centralized AI Providers Service (ai_providers) that handles all AI/LLM integrations. Instead of calling AI APIs directly, use this service for a unified, maintainable approach.
- Unified Interface - Single API for OpenAI, Anthropic, Google AI, OpenRouter, and Ollama
- User Configuration - Users configure their preferred provider and API keys once
- No Duplicate Code - Don't reimplement AI provider logic in each extension
- Centralized Updates - Provider changes update all AI extensions automatically
// 1. Declare dependency on ai_providers
const extensionRoot = new Extension({
name: "my_ai_extension",
version: "1.0.0",
endpoints: [], // No endpoints - ai_providers handles this
requiredAPIKeys: [], // No API keys - ai_providers handles this
author: "your_github",
category: "AI & ML",
dataScope: "full",
dependencies: ["ai_providers"], // Dependency
isService: false
});
// 2. Use the AI service
my_command.execute = function(payload) {
// Check availability
if (typeof callAIProvider === 'undefined') {
return new ReturnObject({status: "error", message: "AI Providers service not available."});
}
const fullText = payload.fullText || "";
// Call AI with custom prompt
const result = callAIProvider(fullText, {
systemPrompt: "Summarize the following text concisely.",
maxTokens: 200,
temperature: 0.7
});
return result;
};callAIProvider(prompt, {
provider: "openai", // Override user's default
model: "gpt-4o", // Override user's model
systemPrompt: "You are...", // Custom instructions
maxTokens: 150, // Max response tokens
temperature: 0.7 // Creativity (0.0-2.0)
})📖 Read full AI Extensions Guide - Complete examples, best practices, and patterns
Service extensions provide reusable functionality for other extensions. The ai_providers extension is the primary example.
Declare the dependency and check availability:
const extensionRoot = new Extension({
name: "my_extension",
version: "1.0.0",
endpoints: [],
requiredAPIKeys: [],
author: "your_github",
category: "Utilities",
dataScope: "none",
dependencies: ["ai_providers"], // Depends on ai_providers
isService: false
});
my_command.execute = function(payload) {
// Always check if service is available
if (typeof callAIProvider === 'undefined') {
return new ReturnObject({status: "error", message: "AI Providers service not available."});
}
// Use the service
const result = callAIProvider(text, options);
return result;
};Set isService: true and export global functions:
const extensionRoot = new Extension({
name: "my_service",
version: "1.0.0",
endpoints: [],
requiredAPIKeys: [],
author: "your_github",
category: "Services",
dataScope: "none",
dependencies: [],
isService: true // This is a service
});
function myServiceFunction(input) {
// ... service logic
return new ReturnObject({status: "success", message: "Processed", payload: result});
}
// Export globally
if (typeof global !== 'undefined') {
global.myServiceFunction = myServiceFunction;
}📖 See AI Extensions Guide for detailed service extension examples
Antinote provides a Math Expression Evaluator API (MathEvaluator) that's available to all extensions. This evaluator uses the same powerful math engine as Antinote's built-in math keyword feature.
- User-Friendly Input - Users can enter
0.05/12instead of0.00416667 - Readability - Expressions like
365/12are clearer than pre-calculated decimals - Consistency - Same math engine across all extensions and core features
- Safety - Evaluated in Swift, not JavaScript
eval()- no code execution risks
The MathEvaluator object provides these methods:
Evaluate a math expression string. Throws error if evaluation fails.
const result = MathEvaluator.eval("0.05/12"); // Returns 0.00416667
const value = MathEvaluator.eval("(2+3)*4"); // Returns 20Safely evaluate an expression, returning a default value on error.
const rate = MathEvaluator.evalSafe("0.05/12", 0); // Returns 0.00416667
const bad = MathEvaluator.evalSafe("invalid", 0); // Returns 0 (default)Check if a string looks like a math expression.
MathEvaluator.isMathExpression("5+3"); // true
MathEvaluator.isMathExpression("0.05/12"); // true
MathEvaluator.isMathExpression("42"); // false (just a number)Parse a parameter that could be a number or math expression. This is the most common method you'll use.
const rate = MathEvaluator.parseNumeric("0.05/12"); // Returns 0.00416667
const num = MathEvaluator.parseNumeric(42); // Returns 42
const calc = MathEvaluator.parseNumeric("5*12"); // Returns 60The evaluator supports:
- Basic operators:
+,-,*,/ - Parentheses:
(2 + 3) * 4 - Exponents:
2^3or2**3 - Modulo:
10 % 3 - Decimals:
0.05 / 12 - Negative numbers:
-5 + 3
Add graceful fallback for compatibility:
my_command.execute = function(payload) {
const params = this.getParsedParams(payload);
// Support math expressions with fallback
const parseNumeric = (typeof MathEvaluator !== 'undefined') ?
(val) => MathEvaluator.parseNumeric(val) :
(val) => parseFloat(val);
const rate = parseNumeric(params[0]) / 100; // Supports "5" or "0.05*100"
const months = parseNumeric(params[1]); // Supports "360" or "30*12"
// ... rest of your logic
};const pmt = new Command({
name: "pmt",
parameters: [
new Parameter({type: "number", name: "rate", helpText: "Interest rate per period (e.g., 0.05 for 5% or 0.05/12 for monthly)", default: 0}),
new Parameter({type: "number", name: "nper", helpText: "Number of periods (e.g., 360 or 30*12)", default: 0}),
new Parameter({type: "number", name: "pv", helpText: "Present value (loan amount)", default: 0})
],
type: "insert",
helpText: "Calculate payment per period (Excel PMT function)",
tutorials: [
new TutorialCommand({command: "pmt(0.05/12, 360, 300000)", description: "Monthly payment on $300k mortgage at 5%"}),
new TutorialCommand({command: "pmt(0.06/12, 60, 25000)", description: "Monthly payment on $25k car loan at 6%"})
],
extension: extensionRoot
});
pmt.execute = function(payload) {
const params = this.getParsedParams(payload);
// Support math expressions if MathEvaluator is available
const parseNumeric = (typeof MathEvaluator !== 'undefined') ?
(val) => MathEvaluator.parseNumeric(val) :
(val) => parseFloat(val);
const rate = parseNumeric(params[0]); // Now accepts "0.05/12"!
const nper = parseNumeric(params[1]); // Now accepts "30*12"!
const pv = parseNumeric(params[2]);
// Calculate payment...
const payment = calculatePMT(pv, rate, nper);
return new ReturnObject({status: "success", message: "Payment calculated", payload: payment});
};Before (requires pre-calculation):
::pmt(0.00416667, 360, 300000)
After (readable expressions):
::pmt(0.05/12, 30*12, 300000)
The expression 0.05/12 is immediately recognizable as "5% divided by 12 months" - much clearer than the decimal 0.00416667.
- Go to Preferences > Extensions
- Toggle Enable Extension Logging
- View logs at the bottom of the Extensions preferences
Use standard console methods in your code:
console.log("Debug info:", value);
console.warn("Warning message");
console.error("Error details:", error);These will appear in:
- Extensions Logging UI (if enabled)
- Terminal output (if launched from command line)
Launch Antinote from Terminal to see all logs:
open /Applications/Antinote.appLook for:
JS console.log:- Your extension's console outputExtensions -- Swift-side extension loading logs
Here's a complete extension with preferences, error handling, and best practices:
(function() {
var extensionName = "random";
var extensionRoot = new Extension({
name: extensionName,
version: "1.0.0",
endpoints: [], // No network calls
requiredAPIKeys: [], // No API keys
author: "johnsonfung",
category: "Random Generators",
dataScope: "none" // Doesn't need note content
});
var random_number = new Command({
name: "random_number",
parameters: [
new Parameter({type: "float", name: "from", helpText: "Bottom range", default: 0}),
new Parameter({type: "float", name: "to", helpText: "Top range", default: 100}),
new Parameter({type: "bool", name: "int", helpText: "Round to nearest whole number", default: true})
],
type: "replaceLine",
helpText: "Generate a random number between two values.",
tutorials: [
new TutorialCommand({command: "random_number", description: "Generate random integer 0-100"}),
new TutorialCommand({command: "random_number(10, 20)", description: "Generate random integer 10-20"}),
new TutorialCommand({command: "random_number(10, 20, false)", description: "Generate decimal 10-20"})
],
extension: extensionRoot
});
random_number.execute = function(payload) {
var [from, to, int] = this.getParsedParams(payload);
// Validation
if (from > to) {
return new ReturnObject({status: "error", message: "'from' cannot be greater than 'to'."});
}
if (from < 0 || to < 0) {
return new ReturnObject({status: "error", message: "Values cannot be negative."});
}
// Logic
var result = Math.random() * (to - from) + from;
if (int) {
result = Math.floor(result);
}
return new ReturnObject({status: "success", message: "Random number generated.", payload: result});
};
})();For larger extensions (>500 lines), you can split code across multiple files for better organization and maintainability.
✅ Use multi-file when:
- Extension is >500 lines
- You have reusable helper functions
- Commands can be logically grouped
- Code is becoming hard to navigate
❌ Keep single-file when:
- Extension is <500 lines
- Logic is straightforward
- No clear separation of concerns
Recommended folder structure for official extensions:
my_extension/
index.js # Entry point, setup
helpers/ # Shared utilities
extractor.js
validators.js
formatters.js
commands/ # Command implementations
csv-commands.js
sort-commands.js
filter-commands.js
extension.json # Metadata with files array
README.md
index.test.js
In extension.json, add a files array:
{
"name": "json_tools",
"version": "2.0.0",
...
"files": [
"index.js",
"helpers/extractor.js",
"helpers/validators.js",
"helpers/formatters.js",
"commands/csv-commands.js",
"commands/sort-commands.js",
"commands/filter-commands.js"
]
}Rules:
index.jsmust be first- Files load sequentially in order
- All files are concatenated before execution
- Use relative paths from extension root
Use a shared namespace pattern:
index.js (entry point):
(function() {
const extensionName = "json_tools";
// Create shared namespace
const Shared = {};
const extensionRoot = new Extension({
name: extensionName,
version: "2.0.0",
...
});
// Export for other files
if (typeof __EXTENSION_SHARED__ === 'undefined') {
var __EXTENSION_SHARED__ = {};
}
__EXTENSION_SHARED__[extensionName] = {
root: extensionRoot,
shared: Shared
};
})();helpers/extractor.js:
(function() {
const ctx = __EXTENSION_SHARED__["json_tools"];
// Add utility to shared namespace
ctx.shared.extract = function(text, path) {
// ... extraction logic
return {data, prefix, suffix};
};
})();commands/csv-commands.js:
(function() {
const ctx = __EXTENSION_SHARED__["json_tools"];
const extract = ctx.shared.extract; // Use helper
const extensionRoot = ctx.root;
const csvToJson = new Command({
name: "csv_to_json",
extension: extensionRoot,
...
});
csvToJson.execute = function(payload) {
const result = extract(payload.fullText); // Use shared helper
// ...
};
})();Extension sizes are automatically calculated:
{
"name": "json_tools",
"size": {
"total": 36005, // Total bytes
"totalKB": 35.2 // For display
}
}Run npm run calculate-sizes to update sizes for all extensions.
The validation script checks:
- ✅ All declared files exist
- ✅
index.jsis first - ✅ No undeclared .js files (warnings)
- ✅ Official extensions follow folder structure (helpers/, commands/, utils/)
Run npm run validate to check your extension.
The json_tools extension demonstrates a complete multi-file implementation with smart JSON extraction and flexible data processing.
Structure:
json_tools/
index.js # Entry point, preferences
helpers/
smart-extractor.js # Smart extraction with dot notation
formatters.js # Formatting utilities
commands/
csv.js # CSV conversion
sort.js # Sorting commands
filter.js # Filter commands
keys.js # Key manipulation
dupes.js # Duplicate detection
utils.js # Utility commands
extension.json # Metadata (15 commands)
Smart Extraction Features:
The extension uses a shared SmartExtractor helper that provides:
- Automatic JSON/object detection - Finds first
{or[in text - Dot notation support - Access nested properties like
"user.profile.name" - Multiple object processing - Optional
parent_keyparameter processes ALL matching objects - Format preferences - User-configurable output (pretty/compact/minified)
Example Usage:
Basic extraction (finds first object/array):
Input text:
The response is: [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
Command: ::json_sort("name")
Result: [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
Dot notation (access nested data):
Input text:
{
"user": {
"profile": {
"contacts": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
}
}
}
Command: ::json_sort("age", "", "user.profile.contacts")
Result: Sorts the contacts array by age (finds it via dot notation path)
Processing multiple objects (parent_key parameter):
Input text:
{
"users": [{"name": "Zack", "age": 30}, {"name": "Alice", "age": 25}],
"admins": [{"name": "Bob", "age": 35}],
"guests": [{"name": "Carol", "age": 28}]
}
Command: ::json_sort("name", "", "users")
Result: Sorts ONLY the "users" array (processes all arrays matching "users")
Command: ::json_add_key("status", "active")
Result: Adds "status": "active" to ALL objects in ALL arrays
Key Commands:
All commands accept an optional final parent_key parameter:
::csv_to_json(parent_key?)- Convert CSV to JSON array::json_sort(key, reverse?, parent_key?)- Sort array by key::json_filter(key, operator, value, parent_key?)- Filter matching items::json_add_key(key, value, parent_key?)- Add key to all objects::json_get(path)- Extract value at path::json_set(path, value)- Set value at path::json_format(style?)- Format JSON (pretty/compact/minified)
Shared Namespace Pattern:
// index.js - Create shared namespace
const globalScope = (typeof global !== 'undefined') ? global :
(typeof window !== 'undefined') ? window : this;
if (typeof globalScope.__EXTENSION_SHARED__ === 'undefined') {
globalScope.__EXTENSION_SHARED__ = {};
}
globalScope.__EXTENSION_SHARED__["json_tools"] = {
root: extensionRoot,
shared: Shared
};
// helpers/smart-extractor.js - Export utilities
const ctx = globalScope.__EXTENSION_SHARED__["json_tools"];
ctx.shared.SmartExtractor = {
extract, extractAll, getNestedValue, setNestedValue, format, reconstruct
};
// commands/sort.js - Use shared utilities
const ctx = globalScope.__EXTENSION_SHARED__["json_tools"];
const SmartExtractor = ctx.shared.SmartExtractor;
const extensionRoot = ctx.root;
json_sort.execute = function(payload) {
const [key, reverse, parent_key] = this.getParsedParams(payload);
// Extract all matching objects/arrays
const extractions = SmartExtractor.extractAll(payload.fullText, parent_key);
// Process each one
const newValues = extractions.map(ext => {
return ext.processed.sort((a, b) => /* sort logic */);
});
// Reconstruct text with all updates
return SmartExtractor.reconstructAll(payload.fullText, extractions, newValues, formatPref);
};See /extensions-official/json_tools/ for the complete implementation.
We welcome community contributions! To submit your extension:
- Fork this repository
- Create your extension folder in
extensions-unofficial/your-extension-name/with:index.js- Your extension codeextension.json- Metadata (name, version, author, category, dataScope, endpoints, requiredAPIKeys, commands)README.md- Documentation and usage examplesindex.test.js- Test file to verify your extension works
- Run validation and tests locally before submitting:
npm install npm run validate # Validates metadata and security disclosures npm run test # Runs all validation and extension tests
- Fix any validation errors - All extensions must:
- Include all required metadata fields
- Declare all API endpoints they call
- Declare all API keys they require
- Have accurate data scope declarations
- Pass their own test suite
- Submit a pull request with:
- All four required files (index.js, extension.json, README.md, index.test.js)
- Clear description of what it does
- Usage examples
To become an official extension (included with Antinote by default):
- Start as an unofficial extension
- Demonstrate community value and usage
- Must meet criteria:
- Aligns with Antinote's simplicity philosophy
- Provides clear, focused value
- Well-documented and tested
- Follows security best practices
- Declares minimal necessary data scope
- Passes all validation and security disclosure checks
Extension({name, version, endpoints, requiredAPIKeys, author, category, dataScope, dependencies, isService})
Creates extension container.
- dependencies (optional): Array of extension names this depends on
- isService (optional): Boolean - true if providing services to other extensions
Registers a command (note: aliases parameter has been removed).
Defines command parameter.
- type:
"float","int","bool","string","paragraph","expression","mathExpression" - default: Default value for autocomplete/tutorials and optional parameters
- required: Whether user must provide value (defaults to
true)
Example usage for users.
User-configurable preference.
- Types:
"bool","string","paragraph","selectOne","selectMultiple"
Standard return value.
- Status:
"success"or"error"
Parses parameters based on types and applies defaults.
Gets preference value for an extension.
Makes authenticated API call (for network extensions).
- Extension identity is automatically determined by the system for security
- Cannot be spoofed by malicious extensions
Evaluates a math expression string and returns the result. Throws error if evaluation fails.
Safely evaluates a math expression, returning defaultValue if evaluation fails.
Returns true if string contains math operators, false otherwise.
Parses a value that could be a number or math expression. Most commonly used method for handling numeric parameters that support expressions.
- Extensions run in a sandboxed JavaScript context
- API keys stored securely in macOS Keychain
- Users can see what data each extension accesses
- Network extensions clearly marked in settings
- All API endpoints and keys visible to users before use
- Choose minimal data scope - Use
"none"or"line"when possible - Validate all inputs - Return clear error messages
- Use console.log() for debugging - Enable logging in settings
- Provide good examples - Tutorial commands help users understand
- Handle errors gracefully - Always return a ReturnObject
- Use modern JavaScript - ES6 features available (const/let, arrow functions, template literals, etc.)
- Document your code - Add comments explaining logic
- Test edge cases - Invalid inputs, empty values, etc.
For a complete list of all official extensions and their commands, see: Official Extensions Index
This catalog includes:
- All extensions organized by category
- Complete command reference with parameters
- Usage examples for every command
- Data scope and network access information
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Examples: Check
extensions-official/for reference implementations
Happy extending! 🚀