Skip to content

Developing Alisio plugins ​

Español · English

This guide covers first-party packages in this monorepo and independent plugins published from another repository. Choose the path first; the plugin contract and safety limits apply to both. The canonical upstream reference for the SDK and PluginAPI is the Alisio plugin documentation. This catalog guide focuses on the packaging, publication and listing workflow rather than duplicating the complete API reference.

Everything in this guide was checked against the upstream source (docs/plugins.md, packages/sdk/src/index.ts, packages/core/src/plugins/host.ts, packages/core/src/runtime/modules.ts, packages/core/src/plugins/install.ts). Where the upstream documentation and the source disagree, this guide follows the source and calls it out as a documentation caveat.

Choose your path ​

You are building…Work here?Release and catalog behavior
An official, first-party @alisio/plugin-* packageYes — create packages/<name>/.The workspace checks, Changesets, release tooling, and automatic local catalog discovery apply.
An independent plugin owned by your teamNo — use your own repository and publish it to npm.You choose your tooling and release process. It is installable after npm publication; catalog listing requires maintainer review and a registry pull request.

Universal ecosystem recommendations are marked below. Rules described as first-party only are enforced by this repository's workspace tooling and must not be copied as claims about every external plugin.

Third-party quick path ​

Use this path when your package belongs in another repository. It does not require a fork of this monorepo.

  1. Create an ESM Node package and implement the default definePlugin(...) export described in the contract.
  2. Build JavaScript and declarations into dist, test the built package, and inspect the tarball.
  3. Publish the package to npm, then install its published version with Alisio and run plugins doctor.
  4. If you want a listing here, follow Catalog inclusion. npm publication alone does not add a package to this catalog.

External package baseline ​

These are the compatible, recommended defaults for an independently published plugin. The host requires the alisio-plugin keyword to recognize an npm package; the other entries are ecosystem or quality recommendations, except where your own package policy makes them mandatory.

ConcernRecommended external-package baseline
NamePrefer @your-scope/alisio-plugin-<name>. @alisio/plugin-* is reserved for first-party packages; the catalog registry can list any valid npm package name.
MetadataProvide description, license, repository, homepage, and bugs so users and catalog maintainers can assess the package.
Host SDKPut @alisio/sdk in both peerDependencies and devDependencies; do not import @alisio/core or another plugin.
Runtime and modulesUse ESM ("type": "module"), target Node >=22.16, and expose ./dist/index.js plus ./dist/index.d.ts.
Published contentsInclude dist, a concise README.md with install/use instructions, an MIT LICENSE (or clearly declared alternative), and every resource or cover the plugin registers.
QualityKeep unit tests; cover a happy path and an error/boundary path for each functional scenario.
Trust and dataPlugins run in-process, not in a sandbox. Validate untrusted names, paths, and child-session output; persist durable data atomically; avoid credentials and machine-specific paths in source and the tarball. Optional built-in integrations should fail open and remain capability-narrowed.

Start from this manifest shape and adapt versions, scripts, and the package manager to your own repository:

json
{
  "name": "@your-scope/alisio-plugin-example",
  "version": "0.1.0",
  "description": "A concise description of the plugin.",
  "keywords": ["alisio-plugin"],
  "license": "MIT",
  "repository": { "type": "git", "url": "https://github.com/OWNER/REPOSITORY.git" },
  "homepage": "https://github.com/OWNER/REPOSITORY#readme",
  "bugs": { "url": "https://github.com/OWNER/REPOSITORY/issues" },
  "type": "module",
  "engines": { "node": ">=22.16" },
  "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
  "files": ["dist", "README.md", "LICENSE", "cover.svg", "icon.svg"],
  "peerDependencies": { "@alisio/sdk": ">=0.1.0-alpha.10 <0.2.0" },
  "devDependencies": { "@alisio/sdk": "0.1.0-alpha.10" }
}

Third-party release checklist ​

  • [ ] npm pack --dry-run shows dist/index.js, dist/index.d.ts, README.md, LICENSE, and registered resources.
  • [ ] Your repository's lint, type, test, and build checks pass; scan the packed tarball for credentials and local paths.
  • [ ] The published package has the alisio-plugin keyword and an entry Alisio can resolve.
  • [ ] Publish with your authorized npm workflow; this monorepo's Changesets and publish-* scripts are first-party only.
  • [ ] Verify the registry result: npm view <package> version.
  • [ ] Verify the consumer path: alisio install npm:<package>@<version>, then alisio plugins doctor.
  • [ ] If applicable, verify configured credentials stay in Alisio/the environment rather than the package, and document the minimum compatible Alisio runtime.

Catalog inclusion ​

This catalog is not npm discovery. Local packages under packages/* are discovered automatically; an external package appears only after a maintainer accepts a pull request to this repository that adds it to registry/plugins.json. There is no separate issue form or automated submission endpoint in this repository.

After publishing, open a pull request against GustavoGutierrez/alisio-plugins that edits registry/plugins.json and includes the requested review information below. Maintainers run the scanner and commit its generated cache/pages; do not hand-edit generated catalog files.

The registry schema requires only package. Its optional fields are title, npmUrl, repository, homepage, cover, icon, categories, and featured — exactly as defined in registry/plugins.schema.json.

json
{
  "package": "@your-scope/alisio-plugin-example",
  "categories": ["tools"],
  "cover": "cover.svg"
}
Maintainer review informationHow it is used or verified
npm package name and published version; install commandpackage is the sole required registry field. The catalog derives alisio install npm:<package> and reads the latest npm version. Include the version you tested in the pull request.
Repository URL, homepage, bugs URL, description, and licenseThe scanner reads npm metadata; registry repository and homepage can override missing/incorrect packument values. bugs, description, and license have no registry override.
Category and capabilitiescategories is an optional registry override. Use only: model-provider, methodology-harness, memory, subagents, search, tools, security, analytics, mcp, storage, ui, decisions. Explain actual capabilities in the PR; capabilities are not a schema field.
Peer SDK range, Node compatibility, tests and validation evidenceThese are maintainer review evidence, not registry fields. Include your @alisio/sdk peer range, engines.node, commands/results, and the exact published version installed with Alisio.
Security and integration detailsExplain network/process/write behavior, credential handling, durable-data behavior, optional integrations, and any resources. The scanner never imports or executes third-party code.
Cover and presentationcover is optional. It may be a package-relative path such as cover.svg, or a /... path to a file already committed under site/public. Otherwise the scanner checks package.json alisio.cover, then root cover.svg, .png, .jpg, .jpeg, .webp. Prefer a 16:9, 1600×900 SVG under 512 KB and ensure it is included in the npm tarball. An optional square icon follows the same path rules, then package.json alisio.icon, falling back to a root icon.svg, .png or .webp. See Cover and icon artwork.

npmUrl, title, and featured are optional presentation overrides, not evidence substitutes. The scanner fetches npm metadata and, only to find a cover, reads the published tarball without executing it. A maintainer can reject or defer a registry change when the package, metadata, safety, or validation evidence is insufficient.

Cover and icon artwork ​

Every plugin can ship two images: a wide cover for the catalog card and detail page, and a square icon for compact lists, the catalog JSON and host UIs. Both are optional, but first-party packages ship both. Put them at the package root as cover.svg and icon.svg and list both in package.json files, so the scanner finds them locally and in the published tarball. Also declare the icon in package.json so Alisio (>= 0.11.2) can show it for an installed plugin even when no catalog entry is available:

json
{
  "files": ["dist", "README.md", "LICENSE", "cover.svg", "icon.svg"],
  "alisio": { "icon": "icon.svg" }
}

alisio.icon must be an image path inside the package; the host rejects files over 256 KB. The catalog scanner honors the same field, after a registry icon and before the root icon.* convention.

PropertyCoverIcon
Aspect ratio16:91:1 (square)
Recommended size1600 × 900512 × 512
Minimum raster size640 × 360128 × 128
SVG viewBox0 0 1600 9000 0 512 512
Formats, in lookup ordersvg, png, jpg, jpeg, webpsvg, png, webp
Maximum file size512 KB64 KB
Default file namecover.<ext>icon.<ext>
Missing fileDefault cover is usedicon: null; no fallback
Published to the site at/covers/<slug>.<ext>/icons/<slug>.<ext>

Design rules for icons:

  • Bake the background into the file: a full-bleed rounded square (rx ≈ 112 on a 512 viewBox). Hosts show the icon as-is, on light and dark themes.
  • Use one bold glyph that stays legible at 32 px. Avoid text, hairline strokes and fine detail.
  • Derive colors and motif from the plugin's cover so both read as the same brand.
  • Keep SVGs self-contained: no <image>, scripts, external references or web fonts, and prefix gradient and filter ids with the plugin name so several icons can be inlined on one page.
  • A plugin that integrates a specific vendor or library (for example OpenAI, Atlassian or Brave) may use that vendor's official mark, unmodified in shape and proportions and in the colors its brand guidelines allow, on the icon's background. Record the mark owner and source in an XML comment at the top of the file. Never imply endorsement beyond the integration itself.

Catalog JSON ​

Every site build publishes a minified, machine-readable catalog at https://gustavogutierrez.github.io/alisio-plugins/catalog.json. The Alisio desktop app reads it to list, search, filter and install plugins without the CLI. It is derived from the committed cache, so it changes only when the cache is refreshed. Image URLs are built from the site URL, defined once in scripts/lib/catalog.mjs; set ALISIO_SITE_URL (an absolute https URL) at build time to publish under another domain.

The top level carries schemaVersion (1), generatedAt (the newest publishedAt, or null), site, categories ({ id, count } for each known category in use) and plugins, sorted by slug. Each plugin carries:

FieldMeaning
id, slug, namePlugin id (or null), URL slug, display name.
package, versionnpm package to install and its catalog version.
descriptionOne-line summary.
categories, tagsFilter facets: known category ids, plus keywords without alisio-plugin or category ids.
cover, iconAbsolute image URLs, or null.
sdkThe @alisio/sdk peer range, or null.
author, license, homepage, npmUrl, repositoryDisplay metadata; links are http(s) or null.
publishedAt, featured, sourcePublish time, promotion flag, local or third-party.

First-party quick path ​

bash
corepack enable
pnpm install
pnpm check                 # lint, types, tests, build, pack check
pnpm diagrams:check        # committed SVGs are newer than their sources

This is the first-party-only path. Scaffold under packages/<name>/ following the conventions in the root README and CONTRIBUTING.md.

The contract ​

A plugin is an ES module whose default export is a Plugin object. Use the SDK's definePlugin helper (an identity function that only adds typing) and textResult to build tool results.

ts
import { definePlugin, textResult } from "@alisio/sdk";

export default definePlugin({
  id: "acme.hello",
  name: "Acme Hello",
  description: "Adds a friendly greeting tool",
  version: "0.1.0",
  apiVersion: 1,
  setup(api) {
    api.tools.register({
      name: "hello",
      description: "Greets the user.",
      effect: "read",
      inputSchema: { type: "object", properties: {}, additionalProperties: false },
      async execute() {
        return textResult("Hello!");
      },
    });
  },
});

The host validates and enforces this contract:

MemberRequiredRule
idyesMatches ^[a-z0-9][a-z0-9.-]{0,63}$, e.g. acme.hello.
versionyesStrict semver: ^\d+\.\d+\.\d+(?:-[\w.-]+)?$ (a pre-release suffix is allowed; build metadata is not).
apiVersionyesExactly 1.
setup(api)yesA function; may be async. Registers everything.
name, descriptionnoNon-empty human-friendly catalog text used by /plugins.
categoriesnoOptional catalog categories: model-provider or methodology-harness. The host validates the value strictly (see below) and also derives model-provider from provider registrations.
extensionsnoDeclarative extension providers, registered at priority 0 before setup runs.
dispose()noReleases resources when Alisio closes.

What the host guarantees:

  • Duplicate ids are rejected (Duplicate plugin: <id>).
  • Contract validation runs before setup. An invalid id, version, or apiVersion fails the load.
  • setup failures roll back every registration, then call dispose(). Every register/on call returns an unregister function; the host reverses all of them on failure and then awaits dispose() before rethrowing.
  • Everything a plugin registers is removed automatically when it unloads.

Catalog categories and host compatibility ​

categories is one of the few fields the host validates against a closed set (the SDK's PluginCategory). Validation is strict: a host that does not know a declared value fails the load with a contract error instead of ignoring the unknown value. The known values are model-provider (the original value) and methodology-harness (@alisio/sdk 0.1.0-alpha.10 / @alisio/core0.1.0-alpha.15 and newer).

A plugin cannot express a host-version dependency. Plugins depend on @alisio/sdk only, never on @alisio/core, and the SDK peer range is not a host version the host enforces. So declaring a category that a newer SDK introduced fails the load on older hosts, and no manifest field prevents it. Document the minimum Alisio runtime in your README and give the upgrade command (npm install -g @alisio/alisio-code@<version>, or pnpm add -g / bun add -g).

Capability surface ​

Every namespace a plugin receives on api, what it gives you, how names are scoped, and how the effect/permission model applies.

NamespaceWhat it gives youNaming / namespacingEffect and permissions
api.tools.register(tool)Tools the model can call.External plugins get p_<10-hex>_<name> (10 hex chars of the SHA-256 of the plugin id); built-ins stay unprefixed. Must match ^[a-zA-Z0-9_-]{1,64}$ after prefixing.Carries effect (read/write/process/external/internal). read is always allowed; write/process/external are gated by policy or approval.
api.commands.register(name, handler, options?)Slash commands.External: <plugin id>:<name> (e.g. /acme.hello:name or /command acme.hello:name); built-ins stay unprefixed. Duplicate names throw.Not effect-gated; still runs in-process with full privileges.
api.events.on(handler)Observe versioned RunEvents (schemaVersion, runId, sessionId, seq, type, timestamp, data).Per plugin.Read-only; a throwing observer cannot break the run.
api.context.register(provider)() => Promise<string> text added to the model context.Per plugin.Not gated.
api.resources.skills(path) / .prompts(path) / .agents(path)Register resource directories relative to the plugin file.Directories are tagged with the plugin id.Not gated. Skills and prompt templates are visible to the host in the same boot; agent definitions are not (see hard limits).
api.resources.list(kind)The directories every plugin registered for skills, prompts or agents, with the owner plugin id.—Read-only view.
api.state.get(key) / .set(key, value)Small JSON state per plugin, persisted in the session database.Scoped to the plugin id.Not gated.
api.storage.sqlite(path)A private (0600) SQLite file, creating parent directories (0700). Returns the synchronous SqlDatabase port (exec, prepare, transaction, close; prepared statements cached by SQL text; FTS5 available).Caller-supplied path.Not gated; the database is closed when the plugin unloads unless the plugin closed it first.
api.compaction.register({ beforeCompact, afterCompact })Contribute instructions, output fields, injected context and a report to core compaction.Per plugin.Host timeout 15 s; a failure or timeout is recorded as a plugin failure and the core continues.
api.session.onStart(handler)Text injected once at the start of a new, empty session (persisted).Per plugin.Host timeout 15 s; failures isolated.
api.session.onEnd(handler)Called when an interactive session ends (/clear, /exit, quit).Per plugin.Host timeout 10 s; handlers run concurrently; failures isolated.
api.model.complete(request)Provider-agnostic text completion. Plugins never import provider SDKs.Per plugin.Hard 120 s timeout; does not count against limits.maxTokens.
api.models.list(signal?) / .resolve(reference, signal?)Credential-free access to configured /connect models; resolves canonical provider/model or a unique bare id.Per plugin.Not gated.
api.providers.register(provider)Add selectable model-provider metadata and a factory. Registrations coexist; /connect selects one.Unique by provider id.Not gated by effect; provider credentials stay in the host.
api.sessions.* (spawn, create, run, get, children, ancestors, cancel, enqueue, isRunning, capabilities, model, workspace, setStatus)Child sessions: separate persisted conversations with fresh context and narrowed permissions.—A child can never exceed its parent; aborting a parent aborts running descendants.
api.ui.status(key, text, detail?) / .panel(id, provider) / .select(req) / .askQuestions(req) / .open(sessionId) / .interactive()Status text, tree panels, and interactive prompts.Status keyed plugin:key; panels keyed plugin:id.Interactive UIs only; headless calls resolve to undefined/false and never hang.
api.extensions.register(point, provider, options?)Replace a typed host extension point: mascot, startup-screen, or websearch.Resolution: highest priority, then plugin id, then registration order.Not gated by effect; a slow or throwing provider falls back to the default.
api.apps?.register(app) (Alisio >= 0.11.5)A full-page plugin app: a sandboxed iframe that replaces the main chat area, listed in the left panel's Apps group, plus typed actions the frame calls. Returns an unregister function.app.id matches ^[a-z][a-z0-9-]{0,39}$, unique per plugin; action names ^[A-Za-z][A-Za-z0-9_.-]{0,63}$.Every action declares effect (read/write/process) and an inputSchema; the host validates the input and applies the session's effect gate. From Alisio 0.11.6 an effect the chat would ask about shows the same approval prompt; a denial is not_permitted with the denied effect. See Plugin apps.

The namespacing rule in one sentence: external plugins are namespaced (tool names prefixed, commands under <plugin id>:) and their internal effect is downgraded to external; built-in plugins are unprefixed and may declare internal.

Plugin apps ​

api.apps (Alisio >= 0.11.5) lets a plugin open a full page in the host's main area. Use an app when people need a working surface of their own (an editor, a studio, a dashboard); use a tool when the model should act, a command for a one-shot action, and a view for read-only output in the chat.

Feature-detect and state the minimum host. api.apps is optional. Register everything else first and add the app only when it exists; never open a separate UI as a fallback. A plugin that cannot work without its app sets its peer range to @alisio/sdk >=0.11.5 <0.12.0.

ts
export default definePlugin({
  id: "acme.notes",
  setup(api) {
    // Tools, commands and views first, so they work on every host.
    if (!api.apps) return; // Explain in /help that the studio needs Alisio >= 0.11.5.
    api.apps.register({
      id: "notes",
      label: { en: "Notes", es: "Notas" },
      html: () => renderAppHtml(), // self-contained, <= 8 MiB
      actions: {
        "notes.list": {
          effect: "read",
          inputSchema: { type: "object", properties: {}, additionalProperties: false },
          handler: async (_input, ctx) => listNotes(ctx.workspace, ctx.signal),
        },
      },
      busy: () => jobs.running(),
      dispose: () => jobs.cancelAll(), // synchronous and idempotent
    });
  },
});

Lifecycle. Register in setup and run inference only later, inside handlers. The host removes the app and calls dispose when the plugin closes or setup fails. Keep state per plugin activation, with no global disposers shared between workspaces. Return true from busy while background work runs so the workspace is not evicted.

Actions. The host validates input against inputSchema ($ref, $id and $async are not supported), then applies the same effect gate as tools. An effect the session already grants runs. From Alisio 0.11.6, when the chat would ask, the host shows the same approval prompt (allow once, allow for the session, deny) labelled with the plugin, app and action; "allow for the session" widens the session exactly like the chat. A denial, an unanswered prompt or a read-only session returns { type: "response", id, error: "not_permitted", effect: "write" | "process" }, so tell the user which permission is missing. The host is the only authority: do not re-check api.sessions.capabilities inside a handler, because it reflects standing grants only and would reject an action the user just allowed once. In Alisio 0.11.5 there is no prompt: anything not already granted is rejected. The session comes from the authenticated route, never from the input. Each call times out after 30 s, counted from when the handler starts (not while the approval prompt waits, which is capped at 10 minutes), and aborts ctx.signal: long work returns a job id at once and the frame polls for it. Results must be JSON-serializable, and errors never echo inputs or secrets. Inside handlers use api.models, api.model.complete and api.sessions; reach other plugins only through a child session with an explicit allowed-tools list. Host limits (PLUGIN_APP_LIMITS): 8 MiB HTML, 37 MiB request, 90 MiB result, 8 requests in flight.

The frame. The HTML runs in a sandboxed iframe with an opaque origin (allow-scripts allow-downloads allow-modals allow-popups) under a strict CSP:

RuleConsequence
script-src 'nonce-…'Every inline <script> carries nonce="__ALISIO_NONCE__"; the host replaces it on each load.
No allow-forms, form-action 'none'A <form> submit does nothing; use buttons and Enter-key handlers.
default-src 'none', connect-src blob:No fetch, XHR or external script, style or font URLs; inline everything and use data:/blob: for assets. All data goes through actions.
No 'unsafe-eval' or 'wasm-unsafe-eval'No eval, new Function or WebAssembly. Workers must be blob: URLs.
No allow-same-originNo cookies, no credentials, no access to the parent DOM.

Transport. On load the host posts { type: "alisio.app.connect", locale, visible } with a MessagePort. Accept only the first one, and only when event.source === window.parent. Send { type: "request", id, action, input?, idempotencyKey? } over the port and match { type: "response", id, result } or { type: "response", id, error }. Error codes: not_found, invalid_input, not_permitted, timeout, failed, too_large, too_many_requests, unauthorized, network. Show an "unavailable" state on not_found (the plugin was disabled or removed).

Locale and visibility. Labels take { en, es } with en as the fallback. Follow the host's { type: "locale" } messages instead of shipping a language selector, and pause polling on { type: "visibility", visible: false }.

Host behavior. Opening an app hides the chat but keeps it mounted, so drafts survive. Frames stay mounted while hidden; each project gets its own instance and session. A full reload loses unsaved text, so persist through actions and warn with beforeunload while edits are pending.

Not available yet: declarative navigation, icons or deep routes, a host jobs API or SSE, model.stream, a browser SDK, and file handles. Disabling a plugin blocks new actions but does not close an open port or cancel running jobs.

Testing. Unit-test the action handlers and the HTML builder: every <script> has the nonce placeholder, there are no external URLs and no <form>. Check the page visually with alisio serve from Alisio 0.11.5 or later.

Hard limits: how far plugins reach ​

This is the section to read before loading code you did not write.

LimitWhat it means
No sandbox.Plugins run in-process with the user's full privileges. The effect field is availability metadata, not isolation. A plugin manifest or a subprocess is not a sandbox.
--read-only disables external plugins entirely.They are not loaded. It also disables writes, processes, network tools and MCP. Built-in plugins are unaffected. When a slash-prompt template requires a capability that --read-only withholds, the CLI prints the exact capability flag (--allow-write / --allow-process) and tells the user to run without --read-only; install is refused outright.
Child sessions only narrow.A child can never gain a capability its parent lacks. A read-only parent poisons all descendants: readOnly is inherited (`parent.readOnly
External names are rewritten; internal is downgraded.External tool names get the p_<hash>_ prefix; an external plugin's internal effect becomes external.
Omitting effect means external, not read.In the runner, effect defaults to external (t.effect ?? "external"). Only declare read for tools with no side effects.
paths() is not a write gate.A tool's paths(input) result feeds context resolution (context.beforePaths), so nested instructions load. It does not restrict what the tool may touch.
Any external plugin auto-allows the external policy.When at least one external plugin loads (or any p_-prefixed tool is registered), the runtime external policy turns on. Enabling a plugin is a real widening of what the model may do.
Hook timeouts are host-enforced but cannot preempt sync code.Compaction and session-start hooks: 15 s. Session-end hooks: 10 s. model.complete: a hard 120 s timeout. A timeout aborts the wait and the AbortSignal, but cannot stop blocking synchronous code.
Extension renders are bounded.A provider has a 250 ms rendering budget, 12 lines for a mascot and 60 for a startup-screen. Rendering is synchronous, so a slow provider is replaced by the default after it returns; it cannot be preempted.
Duplicate tool or command names throw.Duplicate tool: <name> and Duplicate command <key>. Tool names must fit ^[a-zA-Z0-9_-]{1,64}$ after prefixing.
Plugins cannot import another plugin or @alisio/core.Depend on Node built-ins and @alisio/sdk only.
External agent resources are not in the built-in subagents catalog in the same boot.The built-in subagents plugin reads api.resources.list("agents") during its own setup, which runs before external plugins activate. Skills and prompt templates are collected after all plugins activate, so they are visible. Workaround: execute child sessions directly with api.sessions.create/.run and load the packaged agent/skill files yourself — the reference plugin @alisio/plugin-wayfinder does exactly this.
Headless UI calls never hang.ui.select and ui.askQuestions resolve to undefined (and every question id to undefined); ui.open returns false; ui.interactive() returns false.
Installation runs npm with your privileges.alisio install runs npm install --prefix …, which may run the package's lifecycle scripts. The agent-facing plugin_install tool uses the process effect and is unavailable under --read-only.

The most likely mistakes ​

SymptomCauseFix
Error: Child sessions are not available yet / Model resolution is not available yet / Model completion is not available yetapi.sessions.*, api.models.* and api.model.complete are bound after plugins activate. Calling them directly inside setup() throws.Defer those calls to tool execution, a command handler, or a lifecycle hook — anything that runs after startup.
Capability denied: external on a tool that obviously just readsYou omitted effect, so it defaulted to external, and the external policy is off.Declare effect: "read" for side-effect-free tools.
Duplicate tool: … / Duplicate command … at loadTwo registrations collide, or an external tool name exceeds 64 chars after the p_<hash>_ prefix.Uniquify names; keep the prefixed length within 64 characters.
Tool works locally, is missing after installThe package lacks "keywords": ["alisio-plugin"], or exports["."]/main/./index.js does not resolve to the built entry.Add the keyword and ship dist with a resolvable entry.
Agent definitions never show up in the subagents catalogExternal agent resources are not visible in the same boot (see hard limits).Register them for catalog interoperability, but run child sessions directly and load the files yourself.
A hook appears to run twice / state is duplicatedsetup is called once per activation; re-registering on reload is expected. Everything is unregistered on unload.Register idempotently or rely on the unload cleanup.
Diagram check fails on CI with no changesdiagrams:check compares mtimes, not hashes; a fresh checkout can make all files share a timestamp.It is a local authoring aid, not a CI gate. Re-render with pnpm diagrams.

Package shape and publishing ​

RequirementDetail
Package nameFirst-party only: @alisio/plugin-*. For an external package, use a distinct npm name (for example @your-scope/alisio-plugin-*); the host and registry do not require that pattern.
Keywordalisio-plugin is required; a package without it is rejected, so a typo cannot load an unrelated package.
Module formatESM. Published plugins must ship JavaScript (plus declarations).
Entry resolutionexports["."] (import, then node, then default), then main, then ./index.js.
Node target>=22.16.
alisio-plugin.jsonRequired when a plugin is a directory given as a path: { "apiVersion": 1, "entry": "./index.js" }, and the entry must stay inside the directory. Optional for an npm package (a package may ship one too).
DependenciesNode built-ins and @alisio/sdk only. @alisio/sdk stays in both peerDependencies and devDependencies. Never import another plugin or @alisio/core.
Shipped filesdist JavaScript and declarations, README, MIT LICENSE, and every registered resource (e.g. .agents, assets).
Versioning hereFirst-party only: Changesets. Bump one package with pnpm bump-one -- <name> <patch|minor|major> --summary "<text>", or run pnpm changeset then pnpm run version (which also runs scripts/sync-versions.mjs). pnpm publish-one -- <name> and pnpm publish-all are dry runs; add --publish for an intentional publish.

No first-party version is published without explicit authorization and npm authentication. This repository stores no secret; see SECURITY.md.

The first-party publish tooling runs a preflight per package: npm authentication (npm login or NPM_TOKEN), a clean committed tree, matching package.json/src/version.ts, a packed tarball free of local machine paths and credentials (pnpm pack:check, which runs the leak scanner over each tarball), and a version not already on the registry. Dist-tags default to latest, or next for prerelease versions. pnpm release:changesets is the Changesets-native, tag-creating flow used by CI; pnpm publish-all is the explicit, preflighted flow.

First-party published packages carry no machine-specific paths and no credentials. The same guarantee is enforced on every pull request by pnpm check: leak:check scans every tracked file and pack:check scans each packed tarball, so both surfaces are covered before a merge.

Installing and smoke-testing ​

alisio install installs an npm package into Alisio's global plugins directory and records the package name (never the resolved path) in the global config's plugins array.

bash
alisio install npm:@alisio/plugin-x            # latest
alisio install npm:@alisio/plugin-x@1.2.3      # pinned
alisio install @alisio/plugin-x                # bare name: same as npm:
alisio install npm:@alisio/plugin-x --update   # refresh to @latest, keep the name
alisio install npm:@alisio/plugin-x -y         # skip the pre-install confirmation
alisio install npm:@alisio/plugin-x --trust-plugin  # same as --yes
AspectBehavior
Accepted specsnpm:<package>[@<version>], or a bare package name. Names allow letters, digits, ., _, -, plus / for scoped names and @ for a version.
Rejected specsUnknown prefixes (git:, file:, registry:, URLs), absolute or .. paths, and shell metacharacters. A failed install never reruns npm silently.
Where it lands<config home>/plugins/node_modules/<package>; the name is added to <config home>/config.json (atomic write, unrelated fields preserved, no duplicates).
Privilegesnpm install may run the package's lifecycle scripts with your user privileges. On an interactive terminal Alisio warns and asks for confirmation; headless runs must pass --yes/--trust-plugin or they fail before npm runs.
Under --read-onlyInstalling is refused and the plugin never loads.

To smoke-test a local build:

bash
alisio --plugin ./dist/index.js                 # explicit trust for this entry
alisio plugins list                             # installed/known plugins
alisio plugins doctor --plugin ./dist/index.js  # tools, commands, extensions, built-ins

alisio plugins install, alisio plugins remove and alisio plugins validate do not exist. Installing is alisio install; removal is editing the global config and the plugins directory; validation happens at load, surfaced by alisio plugins doctor.

SDK version pinning ​

The published @alisio/sdk is 0.1.0-alpha.10 as of this writing. Verify the current version before you pin:

bash
npm view @alisio/sdk version

The upstream docs example pins ^0.1.0-alpha.16, which is stale and ahead of what is published. Prefer a range this repository actually uses:

json
{
  "peerDependencies": { "@alisio/sdk": ">=0.1.0-alpha.10 <0.2.0" },
  "devDependencies": { "@alisio/sdk": "0.1.0-alpha.10" }
}

Testing in this repository ​

  • Unit tests are required for every plugin package.
  • Cover at least one happy path and one unhappy path per functional scenario (positive, and negative/error/boundary/valid-alternative).
  • Strict TDD is an optional, explicitly recorded project decision, not a default. Recording it is the reference plugin's convention, not a repository precondition; a coverage-first workflow is acceptable when the decision is written down.
  • Tests run as part of pnpm check (pnpm -r test), so a failing test blocks review.

Documentation caveats ​

Where the upstream docs and the source disagree, this guide follows the source:

  1. SDK pin example is stale. The upstream docs show ^0.1.0-alpha.16; the published version is 0.1.0-alpha.10 (check with npm view @alisio/sdk version).
  2. extensions.register covers more than the API table says. The PluginAPI table lists (mascot, startup-screen), but the SDK's typed ExtensionPoints map also includes websearch, and the extension points table documents it. Follow the SDK type.
  3. ui.askQuestions is missing from the upstream API table. It exists in the SDK (PluginAPI.ui) and resolves every question id to undefined when no interactive UI is available.
  4. setup rollback also calls dispose. The upstream "Plugin object" text says partial registrations are rolled back; the host first reverses every registration and then calls dispose() before rethrowing.

This guide and its Spanish mirror are kept in sync and must be updated together. The two versions must always describe the same behaviour.

Released under the MIT License. Maintainer: Gustavo Gutiérrez. Brand assets come from the Alisio documentation site (MIT).