Skip to main content

Plugin API Reference

Complete reference for all APIs available to mahpastes plugins.

clips

Manage clipboard entries.

clips.list(filter?)

Returns an array of clips without the data field (for performance).

Parameters:

NameTypeRequiredDescription
filtertableNoOptional filter criteria
filter.content_typestringNoFilter by MIME type (e.g., "image/png")
filter.limitnumberNoMax results (default: 100, max: 1000)
filter.offsetnumberNoSkip first N results (default: 0)

Returns: Array of clip objects or nil, error_message

Clip object (list):

{
id = 123,
content_type = "image/png",
filename = "screenshot.png",
created_at = 1704067200, -- Unix timestamp
is_archived = false
}

Example:

-- Get all clips
local clips = clips.list()

-- Get only images
local images = clips.list({ content_type = "image/png" })

-- Paginate results
local page2 = clips.list({ limit = 10, offset = 10 })

clips.get(id)

Returns a single clip with its data.

Parameters:

NameTypeRequiredDescription
idnumberYesClip ID

Returns: Clip object with data, or nil if not found, or nil, error_message

Clip object (get):

{
id = 123,
content_type = "text/plain",
filename = "note.txt",
created_at = 1704067200,
is_archived = false,
data = "Hello, world!", -- Text content as string
data_encoding = nil -- nil for text content
}

-- For binary content (images, etc.):
{
id = 124,
content_type = "image/png",
filename = "screenshot.png",
created_at = 1704067200,
is_archived = false,
data = "iVBORw0KGgo...", -- Base64-encoded binary
data_encoding = "base64" -- Indicates encoding
}

Example:

local clip = clips.get(123)
if clip then
if clip.data_encoding == "base64" then
local binary = base64.decode(clip.data)
-- Process binary data
else
-- Text content, use directly
log("Content: " .. clip.data)
end
end

clips.create(options)

Creates a new clip.

Parameters:

NameTypeRequiredDescription
optionstableYesClip creation options
options.datastringYesContent (text or base64 for binary)
options.content_typestringNoMIME type (default: "application/octet-stream")
options.filenamestringNoOptional filename
options.data_encodingstringNoSet to "base64" for binary data

Returns: Table with id field (e.g., {id = 123}), or nil, error_message

Size limit: 50MB maximum for clip data.

Example:

-- Create text clip
local result = clips.create({
data = "Hello, world!",
content_type = "text/plain",
filename = "greeting.txt"
})
if result then
log("Created clip with ID: " .. result.id)
end

-- Create image clip from base64
local result = clips.create({
data = base64_image_data,
data_encoding = "base64",
content_type = "image/png",
filename = "generated.png"
})

clips.get_data(id)

Returns raw clip data without metadata. For text content, returns the data as-is. For binary content, returns base64-encoded data.

Parameters:

NameTypeRequiredDescription
idnumberYesClip ID

Returns: data, content_type on success, or nil, error_message

Example:

local data, mime_type = clips.get_data(123)
if data then
log("Content type: " .. mime_type)
-- For binary content, data is base64-encoded
-- For text content, data is plain text
end

clips.create_from_url(url, options?)

Downloads content from a URL and creates a new clip. The URL domain must be in the plugin's network permissions. Redirects are validated against the same domain allowlist.

Parameters:

NameTypeRequiredDescription
urlstringYesURL to download (must be http:// or https://)
optionstableNoOptional settings
options.filenamestringNoOverride filename (also accepts name)
options.content_typestringNoOverride MIME type (also accepts mime_type)

Returns: Table with id field (e.g., {id = 123}), or nil, error_message

Size limit: 50MB maximum download size.

Example:

local result, err = clips.create_from_url("https://example.com/image.png", {
name = "downloaded.png"
})
if result then
log("Created clip: " .. result.id)
else
log("Error: " .. err)
end

clips.update(id, options)

Updates a clip's properties.

Parameters:

NameTypeRequiredDescription
idnumberYesClip ID
optionstableYesFields to update
options.is_archivedbooleanNoArchive status

Returns: true on success, or false, error_message

Example:

-- Archive a clip
clips.update(123, { is_archived = true })

clips.delete(id)

Permanently deletes a clip.

Parameters:

NameTypeRequiredDescription
idnumberYesClip ID

Returns: true on success, or false, error_message

Example:

clips.delete(123)

clips.delete_many(ids)

Deletes multiple clips at once.

Parameters:

NameTypeRequiredDescription
idstableYesArray of clip IDs

Returns: true on success, or false, error_message

Example:

clips.delete_many({123, 124, 125})

clips.archive(id)

Archives a clip (shorthand for update with is_archived = true).

Parameters:

NameTypeRequiredDescription
idnumberYesClip ID

Returns: true on success, or false, error_message


clips.unarchive(id)

Unarchives a clip.

Parameters:

NameTypeRequiredDescription
idnumberYesClip ID

Returns: true on success, or false, error_message


tags

Manage tags and clip-tag associations.

tags.list()

Returns all tags with usage counts.

Returns: Array of tag objects, or nil, error_message

Tag object:

{
id = 1,
name = "screenshot",
color = "#3B82F6",
count = 15 -- Number of clips with this tag
}

Example:

local all_tags = tags.list()
for _, tag in ipairs(all_tags) do
log(tag.name .. ": " .. tag.count .. " clips")
end

tags.get(id)

Returns a single tag by ID.

Parameters:

NameTypeRequiredDescription
idnumberYesTag ID

Returns: Tag object, or nil if not found, or nil, error_message


tags.create(name)

Creates a new tag. Color is automatically assigned from a rotating palette.

If the name contains / separators, intermediate parent tags are auto-created. For example, tags.create("work/client1/projectABC") creates work, work/client1, and work/client1/projectABC (skipping any that already exist). The returned tag object is the leaf tag.

Parameters:

NameTypeRequiredDescription
namestringYesTag name (max 50 characters). Use / to create subtags.

Returns: New tag object (the leaf tag), or nil, error_message

Example:

local tag, err = tags.create("important")
if tag then
log("Created tag with ID: " .. tag.id)
else
log("Error: " .. err)
end

-- Create a subtag hierarchy
local subtag = tags.create("work/client1/projectABC")
-- Also creates "work" and "work/client1" if they don't exist

tags.update(id, options)

Updates a tag's name or color.

Parameters:

NameTypeRequiredDescription
idnumberYesTag ID
optionstableYesFields to update
options.namestringNoNew tag name
options.colorstringNoNew color (hex format, e.g., "#FF0000")

Returns: true on success, or false, error_message

Example:

tags.update(1, { name = "urgent", color = "#EF4444" })

tags.delete(id)

Deletes a tag. Removes the tag from all clips.

Parameters:

NameTypeRequiredDescription
idnumberYesTag ID

Returns: true on success, or false, error_message


tags.add_to_clip(tag_id, clip_id)

Adds a tag to a clip.

Parameters:

NameTypeRequiredDescription
tag_idnumberYesTag ID
clip_idnumberYesClip ID

Returns: true on success, or false, error_message

Example:

-- Auto-tag a clip based on content type
function on_clip_created(data)
local clip = clips.get(data.id)
if clip.content_type:find("image/") then
local tag = find_or_create_tag("screenshot")
tags.add_to_clip(tag.id, clip.id)
end
end

tags.remove_from_clip(tag_id, clip_id)

Removes a tag from a clip.

Parameters:

NameTypeRequiredDescription
tag_idnumberYesTag ID
clip_idnumberYesClip ID

Returns: true on success, or false, error_message


tags.get_for_clip(clip_id)

Returns all tags for a specific clip.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID

Returns: Array of tag objects, or nil, error_message

Example:

local clip_tags = tags.get_for_clip(123)
for _, tag in ipairs(clip_tags) do
log("Tag: " .. tag.name)
end

storage

Plugin-scoped key-value storage. Each plugin has isolated storage that persists across restarts.

storage.get(key)

Retrieves a stored value.

Parameters:

NameTypeRequiredDescription
keystringYesStorage key

Returns: Stored value as string, or nil if not found

Example:

local last_sync = storage.get("last_sync_time")
if last_sync then
log("Last synced: " .. last_sync)
end

storage.set(key, value)

Stores a value. Overwrites existing value if key exists.

Parameters:

NameTypeRequiredDescription
keystringYesStorage key
valuestringYesValue to store

Returns: true on success, or false, error_message

Example:

-- Store a simple value
storage.set("counter", "42")

-- Store complex data as JSON
local config = { enabled = true, threshold = 100 }
storage.set("config", json.encode(config))

storage.delete(key)

Deletes a stored value.

Parameters:

NameTypeRequiredDescription
keystringYesStorage key

Returns: true on success, false on error


storage.list()

Lists all storage keys for this plugin.

Returns: Array of key strings, or nil, error_message

Example:

local keys = storage.list()
for _, key in ipairs(keys) do
log("Key: " .. key)
end

http

Make HTTP requests to allowed domains. Plugins must declare domains in their manifest.

Domain Restrictions

HTTP requests are only allowed to domains listed in the plugin manifest:

Plugin = {
name = "My Plugin",
network = {
["api.example.com"] = {"GET", "POST"},
["webhook.site"] = {"POST"},
["*.cdn.example.com"] = {"GET"}, -- Wildcard subdomain
},
}
  • Each domain must be explicitly declared
  • Wildcard subdomains are supported (e.g., *.cdn.example.com matches v1.cdn.example.com)
  • Allowed HTTP methods must be specified per domain
  • Redirects are validated: each hop is checked against the domain allowlist, and HTTPS is enforced
  • Request timeout: 5 minutes
  • Max response size: 50MB
  • Rate limit: 100 requests per minute

http.get(url, options?)

Performs a GET request.

Parameters:

NameTypeRequiredDescription
urlstringYesFull URL (must be to allowed domain)
optionstableNoRequest options
options.headerstableNoRequest headers as key-value pairs

Returns: Response object, or nil, error_message

Response object:

{
status = 200, -- HTTP status code
headers = { -- Response headers
["Content-Type"] = "application/json",
["X-Custom"] = "value"
},
body = "..." -- Response body as string
}

Example:

local resp, err = http.get("https://api.example.com/data", {
headers = {
["Authorization"] = "Bearer " .. api_key
}
})

if resp then
if resp.status == 200 then
local data = json.decode(resp.body)
-- Process data
else
log("Error: HTTP " .. resp.status)
end
else
log("Request failed: " .. err)
end

http.post(url, options?)

Performs a POST request.

Parameters:

NameTypeRequiredDescription
urlstringYesFull URL
optionstableNoRequest options
options.headerstableNoRequest headers
options.bodystringNoRequest body

Returns: Response object, or nil, error_message

Example:

local resp = http.post("https://api.example.com/upload", {
headers = {
["Content-Type"] = "application/json"
},
body = json.encode({ message = "Hello" })
})

http.put(url, options?)

Performs a PUT request. Same signature as http.post.


http.patch(url, options?)

Performs a PATCH request. Same signature as http.post.


http.delete(url, options?)

Performs a DELETE request.

Parameters:

NameTypeRequiredDescription
urlstringYesFull URL
optionstableNoRequest options
options.headerstableNoRequest headers

Returns: Response object, or nil, error_message


fs

Filesystem access with user permission prompts.

Permission Model

  • Plugins must declare filesystem intent in their manifest
  • First access to a path triggers a user approval dialog
  • User can approve a directory (covers all files and subdirectories within)
  • Approvals are stored in the database and remembered across restarts
  • fs.exists only works within already-approved directories (no prompt, returns false for unapproved paths)
  • Rate limit: 50 filesystem operations per minute

Manifest declaration:

Plugin = {
name = "My Plugin",
filesystem = {
read = true, -- Request read access
write = true, -- Request write access
},
}

fs.read(path)

Reads a file's contents. Triggers permission prompt on first access to a directory.

Parameters:

NameTypeRequiredDescription
pathstringYesAbsolute file path

Returns: File contents as string, or nil, error_message

Size limit: 50MB maximum file size.

Example:

local content, err = fs.read("/Users/me/Documents/notes.txt")
if content then
log("File contents: " .. content)
else
log("Error: " .. err)
end

fs.write(path, content)

Writes content to a file. Creates parent directories if needed.

Parameters:

NameTypeRequiredDescription
pathstringYesAbsolute file path
contentstringYesContent to write

Returns: true on success, or false, error_message

Example:

local success = fs.write("/Users/me/Documents/backup.txt", "Backup content")
if success then
log("File written successfully")
end

fs.list(path)

Lists directory contents.

Parameters:

NameTypeRequiredDescription
pathstringYesAbsolute directory path

Returns: Array of file info objects, or nil, error_message

File info object:

{
name = "document.txt",
is_dir = false,
size = 1024, -- Size in bytes
modified = 1704067200 -- Unix timestamp
}

Example:

local files = fs.list("/Users/me/Documents")
if files then
for _, file in ipairs(files) do
if file.is_dir then
log("[DIR] " .. file.name)
else
log(file.name .. " (" .. file.size .. " bytes)")
end
end
end

fs.exists(path)

Checks if a path exists. Only works within already-approved directories (does not trigger a permission prompt).

Parameters:

NameTypeRequiredDescription
pathstringYesAbsolute path

Returns: true if exists within approved path, false otherwise

Note: Returns false for paths outside approved directories to avoid leaking filesystem information.


toast

Display toast notifications to the user.

toast.show(message, type?)

Shows a toast notification.

Parameters:

NameTypeRequiredDescription
messagestringYesNotification message (max 200 characters)
typestringNoToast type: "info" (default), "success", or "error"

Returns: true if shown, false if rate-limited

Rate limit: 5 toasts per minute per plugin.

Example:

toast.show("Sync complete!", "success")
toast.show("Warning: File not found", "error")
toast.show("Processing...") -- defaults to "info"

task

Track progress of long-running operations. Tasks show a progress indicator in the UI.

task.start(name, total?)

Starts a new task and returns a task ID. Shows a progress bar in the UI.

Parameters:

NameTypeRequiredDescription
namestringYesDisplay name for the task
totalnumberNoTotal number of steps (default: 1). Used with task.progress to track completion.

Returns: Task ID (number)

Example:

local task_id = task.start("Processing images", 5)

task.progress(task_id, current)

Updates a task's progress. The current value is an absolute count relative to the total passed to task.start.

Parameters:

NameTypeRequiredDescription
task_idnumberYesTask ID from task.start
currentnumberYesCurrent step number (0 to total from task.start)

Returns: true on success, or false, error_message

Example:

local task_id = task.start("Processing images", 5)
for i = 1, 5 do
-- Process item...
task.progress(task_id, i)
end
task.complete(task_id)

task.complete(task_id)

Marks a task as completed. Automatically sets progress to the total.

Parameters:

NameTypeRequiredDescription
task_idnumberYesTask ID from task.start

Returns: true on success, or false, error_message

Example:

task.complete(task_id)

task.fail(task_id, error_message?)

Marks a task as failed.

Parameters:

NameTypeRequiredDescription
task_idnumberYesTask ID from task.start
error_messagestringNoError description (default: "Unknown error")

Returns: true on success, or false, error_message

Example:

task.fail(task_id, "API returned an error")

Tasks are automatically cleaned up 5 minutes after completion or failure.


Utility Functions

log(message)

Logs a message to the application log. Useful for debugging.

Parameters:

NameTypeRequiredDescription
messagestringYesMessage to log

Returns: Nothing

Example:

log("Plugin initialized")
log("Processing clip ID: " .. clip.id)

Logs appear as: [plugin:your-plugin-name] Your message


json.encode(table)

Encodes a Lua table to JSON string.

Parameters:

NameTypeRequiredDescription
tabletableYesLua table to encode

Returns: JSON string, or nil, error_message

Example:

local data = { name = "test", count = 42, enabled = true }
local json_str = json.encode(data)
-- Result: '{"name":"test","count":42,"enabled":true}'

Note: Arrays (tables with consecutive integer keys starting at 1) are encoded as JSON arrays. Other tables are encoded as JSON objects.


json.decode(string)

Decodes a JSON string to a Lua table.

Parameters:

NameTypeRequiredDescription
stringstringYesJSON string to decode

Returns: Lua table, or nil, error_message

Example:

local data = json.decode('{"name":"test","count":42}')
log(data.name) -- "test"
log(data.count) -- 42

base64.encode(data)

Encodes a string to base64.

Parameters:

NameTypeRequiredDescription
datastringYesData to encode

Returns: Base64-encoded string

Example:

local encoded = base64.encode("Hello, world!")
-- Result: "SGVsbG8sIHdvcmxkIQ=="

base64.decode(string)

Decodes a base64 string.

Parameters:

NameTypeRequiredDescription
stringstringYesBase64-encoded string

Returns: Decoded string, or nil, error_message

Example:

local decoded = base64.decode("SGVsbG8sIHdvcmxkIQ==")
-- Result: "Hello, world!"

utils.time()

Returns the current Unix timestamp (seconds since epoch).

Returns: Number (Unix timestamp)

Example:

local now = utils.time()
log("Current timestamp: " .. now)

-- Store for later comparison
storage.set("last_run", tostring(now))

utils.sha256(data)

Returns the SHA-256 hex digest of a string.

Parameters:

NameTypeRequiredDescription
datastringYesData to hash

Returns: Hex-encoded hash string


utils.hmac_sha256(key, data)

Returns the HMAC-SHA256 hex digest.

Parameters:

NameTypeRequiredDescription
keystringYesHMAC key
datastringYesData to sign

Returns: Hex-encoded HMAC string


utils.url_encode(s)

URL-encodes a string.

Parameters:

NameTypeRequiredDescription
sstringYesString to encode

Returns: URL-encoded string


utils.url_decode(s)

Decodes a URL-encoded string.

Parameters:

NameTypeRequiredDescription
sstringYesString to decode

Returns: Decoded string


utils.clipboard_write(text)

Writes text to the system clipboard.

Parameters:

NameTypeRequiredDescription
textstringYesText to copy

Returns: true on success, or nil, error_message if clipboard access is not permitted

warning

Requires clipboard = true in the plugin manifest.


metadata

Read and write key-value metadata on clips.

metadata.get(clip_id)

Returns all metadata key-value pairs for a clip.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID

Returns: Table of key-value string pairs, or nil, error_message

Example:

local meta = metadata.get(123)
if meta then
for key, value in pairs(meta) do
log(key .. " = " .. value)
end
end

metadata.set(clip_id, key, value)

Sets a single metadata key-value pair on a clip.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID
keystringYesMetadata key (max 256 characters)
valuestringYesMetadata value (max 4096 characters)

Returns: true on success, or false, error_message

Limits: Max 50 key-value pairs per clip.

Example:

metadata.set(123, "source", "screenshot")
metadata.set(123, "project", "docs")

metadata.delete(clip_id, key)

Removes a single metadata key from a clip.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID
keystringYesMetadata key to remove

Returns: true on success, or false, error_message


metadata.set_bulk(clip_id, table)

Atomically replaces all metadata on a clip with the provided table.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID
tabletableYesKey-value pairs to set (replaces all existing metadata)

Returns: true on success, or false, error_message

Example:

metadata.set_bulk(123, {
source = "camera",
project = "photos",
rating = "5",
})

image

Go-side image processing functions. These operate on clip data directly by clip ID, so you don't need to decode image bytes in Lua.

image.info(clip_id)

Returns dimensions and format of an image clip.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID

Returns: Table with width, height, format, size, and has_exif fields, or nil, error_message


image.resize(clip_id, width, height, opts?)

Resizes an image clip and returns the result as raw data.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesSource clip ID
widthnumberYesTarget width (must be positive). Max 10000.
heightnumberYesTarget height (must be positive). Max 10000.
optstableNoOptions
opts.fitstringNoFit mode: "fill" (default), "contain", or "cover"

Returns: Table with data (base64-encoded image) and mime_type, or nil, error_message


image.overlay_text(clip_id, opts)

Draws text onto an image clip. Uses the embedded IBM Plex Mono font.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesSource clip ID
optstableYesText overlay options
opts.textstringYesText to draw
opts.positionstringNoNamed position: "center", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"
opts.xnumberNoX position (overrides position)
opts.ynumberNoY position (overrides position)
opts.sizenumberNoFont size
opts.colorstringNoHex color (e.g., "#FF0000")
opts.opacitynumberNoText opacity (0.0 to 1.0)

Returns: Table with data (base64-encoded image) and mime_type, or nil, error_message


image.composite(opts)

Creates a canvas and composites multiple image layers onto it. Up to 50 layers.

Parameters:

NameTypeRequiredDescription
optstableYesComposite options
opts.widthnumberYesCanvas width
opts.heightnumberYesCanvas height
opts.layerstableYesArray of layer objects (max 50)

Layer object:

NameTypeRequiredDescription
clip_idnumberYesImage clip ID
xnumberNoX offset on canvas
ynumberNoY offset on canvas
widthnumberNoResize layer width
heightnumberNoResize layer height

Returns: Table with data (base64-encoded image) and mime_type, or nil, error_message


image.convert(clip_id, format, opts?)

Converts an image clip to a different format.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesSource clip ID
formatstringYesTarget format: "png", "jpeg", or "jpg"
optstableNoOptions
opts.qualitynumberNoJPEG quality (1-100, default: 85). Only applies to JPEG output.

Returns: Table with data (base64-encoded image) and mime_type, or nil, error_message

Example:

-- Convert PNG to JPEG at 85% quality
local result = image.convert(123, "jpeg", { quality = 85 })
if result then
clips.create({
data = result.data,
data_encoding = "base64",
content_type = result.mime_type,
filename = "converted.jpg",
})
end

image.dominant_colors(clip_id, count?)

Extracts dominant colors from an image using k-means clustering.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID
countnumberNoNumber of colors to return (default: 5, max: 20)

Returns: Array of hex color strings, or nil, error_message


image.grayscale_pixels(clip_id, width, height)

Returns a flat array of grayscale luminance values (0-255). Useful for ASCII art, perceptual hashing, or image analysis.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID
widthnumberYesTarget sample width
heightnumberYesTarget sample height

Returns: Flat array of luminance values (integers 0-255, length = width * height, row-major order), or nil, error_message

Limit: Max 1 million pixels (width * height).


image.metadata(clip_id)

Returns EXIF metadata from an image clip.

Parameters:

NameTypeRequiredDescription
clip_idnumberYesClip ID

Returns: Table with fields like camera_make, camera_model, lens, iso, aperture, shutter_speed, focal_length, date, and gps (a nested table with latitude and longitude). Missing fields are nil. Returns an empty table for images without EXIF data (e.g., PNGs). Returns nil, error_message on failure.


image.diff(clip_id1, clip_id2)

Compares two images and returns a similarity score plus a visual diff image.

Parameters:

NameTypeRequiredDescription
clip_id1numberYesFirst image clip ID
clip_id2numberYesSecond image clip ID

Returns: Table with similarity (0.0 to 1.0, where 1.0 is identical), diff_data (base64-encoded PNG of the visual diff), and diff_mime_type, or nil, error_message

Image Safety Limits

All image operations enforce these limits:

  • Decompression bomb guard: max 100 megapixels per image
  • Output size guard: max 50MB base64-encoded PNG output

Display result modals to the user from plugin actions.

modal.show(opts)

Opens a modal dialog showing plugin output. Supports markdown, plain text, and image content.

Parameters:

NameTypeRequiredDescription
optstableYesModal options
opts.titlestringYesModal title (max 200 characters)
opts.contentstringYesContent to display (max 1MB)
opts.formatstringYes"markdown", "text", or "image"
opts.copy_datastringNoData placed on clipboard when user clicks Copy (max 1MB)
opts.paste_datastringNoData to create a new clip when user clicks Paste (max 10MB)
opts.paste_data_base64booleanNoIf true, paste_data is base64-encoded binary
opts.paste_namestringNoFilename for the pasted clip
opts.paste_content_typestringNoMIME type for the pasted clip

Returns: true on success, or false, error_message

Only one modal can be open at a time across all plugins. If another modal is already showing, modal.show returns false. Modals from async actions are queued and shown when the guard is released.

Example:

-- Show markdown result
modal.show({
title = "Analysis Results",
content = "## Summary\n\nFound **3** duplicate images.",
format = "markdown",
copy_data = "3 duplicates found"
})

-- Show a generated image
modal.show({
title = "Generated Image",
content = base64_png_data,
format = "image",
paste_data = base64_png_data,
paste_data_base64 = true,
paste_name = "generated.png",
paste_content_type = "image/png"
})

Resource Limits

Plugins operate within strict resource limits to ensure system stability:

ResourceLimit
Execution time30 seconds per handler, 5 minutes for async UI actions
Memory50 MB per plugin
HTTP requests100 per minute
File operations50 per minute
Storage10 MB per plugin (not currently enforced)
Toast notifications5 per minute
Clip data size50 MB maximum
File read size50 MB maximum
HTTP response size50 MB maximum
HTTP request timeout5 minutes
Modal title200 characters
Modal content1 MB
Modal paste_data10 MB
Toast message200 characters
Tag name50 characters

Behavior when limits are exceeded:

  • Execution time: Handler is terminated with a timeout error
  • Rate limits: Operation returns an error message
  • Size limits: Operation is rejected with an error message
  • Toast rate limit: Notification is silently dropped (returns false)

Error Handling

Most API functions return errors as a second value:

local result, err = some_api_function()
if result == nil and err then
log("Error: " .. err)
return
end

Common patterns:

-- Check for nil result
local clip = clips.get(id)
if not clip then
log("Clip not found")
return
end

-- Check boolean success with error
local success, err = clips.delete(id)
if not success then
log("Delete failed: " .. (err or "unknown error"))
end

-- Handle HTTP errors
local resp, err = http.get(url)
if not resp then
log("Request failed: " .. err)
return
end
if resp.status ~= 200 then
log("HTTP error: " .. resp.status)
return
end