Skip to content

Repository files navigation

✨ Antinote Extensions

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.


📚 Documentation

Building Specific Types of Extensions:

Reference:


📦 Extension Basics

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

Single-File vs Multi-File 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.json under "files" array
  • index.js must always be first
  • Files are loaded sequentially in order
  • Total size is calculated automatically

See the Multi-File Extensions section below for details.

Extension Folder

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/Extensions

🚀 Using Extensions

To use an extension:

  1. Download the extension's JavaScript file
  2. Place it in the Extensions folder (or your custom folder)
  3. 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.


🧠 Creating an Extension

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.

Basic Extension Structure

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 Parameters

Extension({name, version, endpoints, requiredAPIKeys, author, category, dataScope, dependencies, isService})

  • name (string): Unique name for your extension
  • version (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 name
  • category (string): Category for organization (e.g., "AI", "Math", "Date & Time", "Utilities")
  • dataScope (string): Data access level - see Data Privacy below
  • dependencies (array, optional): Other extension names this depends on (e.g., ["ai_providers"])
  • isService (boolean, optional): Set to true if this extension provides services for other extensions (default: false)

🔐 Data Privacy & Scopes

Extensions must declare what note content they need access to. This is shown to users in the Extensions settings.

Data Scope Options

  • "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


📝 Creating Commands

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

Command Structure

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
});

Command Types

  • "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

Parameters

new Parameter({type, name, helpText, default, required})

Parameter Properties:

  • type (string): Parameter type - see below
  • name (string): Parameter name (shown to user)
  • helpText (string): Description of the parameter
  • default (any): Default value (for autocomplete/tutorials)
  • required (boolean): Whether the parameter must be provided by the user (defaults to true)

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:

  1. For autocomplete/tutorials - Shows users example values
  2. For optional parameters - Used when user doesn't provide a value

⚙️ Writing the Execute Function

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});
};

Payload Structure

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"fullText is empty string ""
  • "line"fullText is current line only
  • "full"fullText is entire note

Return Object

new ReturnObject({status, message, payload})
  • status: "success" or "error"
  • message: User-friendly message shown in notification
  • payload: Result string (for success) or empty (for error)

🌐 Network Extensions & API Keys

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.

Quick Example

// 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"});
};

Security Features

  • 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


🎛 Extension Preferences

Extensions can define user-configurable preferences that appear in Settings.

Registering Preferences

// 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);

Preference Types

  • "bool" - Toggle on/off
  • "string" - Free text input
  • "paragraph" - Multi-line text editor (textarea)
  • "selectOne" - Dropdown with single selection
  • "selectMultiple" - Multiple checkboxes

Using Preferences in Code

my_command.execute = function(payload) {
  // Get preference value
  var model = getExtensionPreference("openai", "model") || "gpt-4o";

  console.log("Using model:", model);
  // ... use in your logic
};

🤖 AI & LLM Extensions

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.

Why Use the AI Service?

  • 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

Quick Example

// 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;
};

Available Options

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 & Dependencies

Service extensions provide reusable functionality for other extensions. The ai_providers extension is the primary example.

Using a Service

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;
};

Creating a Service

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


🧮 Math Expression Evaluator

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.

Why Use the Math Evaluator?

  • User-Friendly Input - Users can enter 0.05/12 instead of 0.00416667
  • Readability - Expressions like 365/12 are 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

Available Methods

The MathEvaluator object provides these methods:

MathEvaluator.eval(expression)

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 20

MathEvaluator.evalSafe(expression, defaultValue)

Safely 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)

MathEvaluator.isMathExpression(str)

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)

MathEvaluator.parseNumeric(value)

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 60

Supported Expressions

The evaluator supports:

  • Basic operators: +, -, *, /
  • Parentheses: (2 + 3) * 4
  • Exponents: 2^3 or 2**3
  • Modulo: 10 % 3
  • Decimals: 0.05 / 12
  • Negative numbers: -5 + 3

Usage in Extensions

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
};

Example: Finance Command

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});
};

Benefits for Users

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.


🐞 Debugging

Enable Extension Logging

  1. Go to Preferences > Extensions
  2. Toggle Enable Extension Logging
  3. View logs at the bottom of the Extensions preferences

Console Logging

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)

Terminal Debugging

Launch Antinote from Terminal to see all logs:

open /Applications/Antinote.app

Look for:

  • JS console.log: - Your extension's console output
  • Extensions - - Swift-side extension loading logs

📚 Complete Example

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});
  };
})();

📁 Multi-File Extensions

For larger extensions (>500 lines), you can split code across multiple files for better organization and maintainability.

When to Use Multi-File

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

Structure

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

Declaring Files

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.js must be first
  • Files load sequentially in order
  • All files are concatenated before execution
  • Use relative paths from extension root

Sharing Code Between Files

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
    // ...
  };
})();

Size Tracking

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.

Validation

The validation script checks:

  • ✅ All declared files exist
  • index.js is first
  • ✅ No undeclared .js files (warnings)
  • ✅ Official extensions follow folder structure (helpers/, commands/, utils/)

Run npm run validate to check your extension.

Example: json_tools

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:

  1. Automatic JSON/object detection - Finds first { or [ in text
  2. Dot notation support - Access nested properties like "user.profile.name"
  3. Multiple object processing - Optional parent_key parameter processes ALL matching objects
  4. 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.


🤝 Contributing

We welcome community contributions! To submit your extension:

  1. Fork this repository
  2. Create your extension folder in extensions-unofficial/your-extension-name/ with:
    • index.js - Your extension code
    • extension.json - Metadata (name, version, author, category, dataScope, endpoints, requiredAPIKeys, commands)
    • README.md - Documentation and usage examples
    • index.test.js - Test file to verify your extension works
  3. 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
  4. 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
  5. 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

⚠️ PR Requirements: All pull requests must pass the automated validation and test suite before being reviewed. The CI workflow will automatically run when you submit your PR. If the checks fail, please fix the issues and push updates.

Official Extensions

To become an official extension (included with Antinote by default):

  1. Start as an unofficial extension
  2. Demonstrate community value and usage
  3. 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

📖 API Reference

Constructors

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

Command({name, parameters, type, helpText, tutorials, extension})

Registers a command (note: aliases parameter has been removed).

Parameter({type, name, helpText, default, required})

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)

TutorialCommand({command, description})

Example usage for users.

Preference({key, label, type, defaultValue, options, helpText})

User-configurable preference.

  • Types: "bool", "string", "paragraph", "selectOne", "selectMultiple"

ReturnObject({status, message, payload})

Standard return value.

  • Status: "success" or "error"

Functions

this.getParsedParams(payload)

Parses parameters based on types and applies defaults.

getExtensionPreference(extensionName, key)

Gets preference value for an extension.

callAPI(apiKeyId, url, method, headers, body)

Makes authenticated API call (for network extensions).

  • Extension identity is automatically determined by the system for security
  • Cannot be spoofed by malicious extensions

MathEvaluator.eval(expression)

Evaluates a math expression string and returns the result. Throws error if evaluation fails.

MathEvaluator.evalSafe(expression, defaultValue)

Safely evaluates a math expression, returning defaultValue if evaluation fails.

MathEvaluator.isMathExpression(str)

Returns true if string contains math operators, false otherwise.

MathEvaluator.parseNumeric(value)

Parses a value that could be a number or math expression. Most commonly used method for handling numeric parameters that support expressions.


🔒 Security & Privacy

  • 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

💡 Tips & Best Practices

  1. Choose minimal data scope - Use "none" or "line" when possible
  2. Validate all inputs - Return clear error messages
  3. Use console.log() for debugging - Enable logging in settings
  4. Provide good examples - Tutorial commands help users understand
  5. Handle errors gracefully - Always return a ReturnObject
  6. Use modern JavaScript - ES6 features available (const/let, arrow functions, template literals, etc.)
  7. Document your code - Add comments explaining logic
  8. Test edge cases - Invalid inputs, empty values, etc.

📚 Extension Catalog

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

📞 Support


Happy extending! 🚀

About

No description, website, or topics provided.

Resources

Stars

97 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages