---
name: sliprail-extension
description: "Building or changing a Sliprail extension that adds commands to the Sliprail desktop launcher, a package whose sliprail.json has an extension section."
---

# Sliprail extension conventions

Extensions add commands to the Sliprail desktop launcher on Windows and macOS. Their code runs with Node.js in the main process of the Sliprail app.

## Package

A package is one directory with sliprail.json at its root:

```json
{
  "displayName": { "en": "Word Counter", "zh-hans": "字数统计" },
  "description": { "en": "Counts the words of the copied text", "zh-hans": "统计复制文本的字数" },
  "icon": "icon.svg",
  "extension": { "main": "main.mjs" }
}
```

- displayName and description are a string or a map of the languages en, zh-hans with en required; give every visible text in the same languages.
- id identifies the extension and its data. The first publish assigns it when sliprail.json has none; keep it unchanged afterwards.
- extension.main is an ES module relative to the package root. Sliprail does not install dependencies, so write plain JavaScript or bundle the build output with every runtime dependency, use Node.js built-in modules freely and import types of @sliprail/sdk with import type only.
- For the exact types of the extension object and the context, add @sliprail/sdk as a dev dependency and read node_modules/@sliprail/sdk/lib/index.d.ts and the files it imports. In plain JavaScript, annotate the default export with /** @type {import('@sliprail/sdk').Extension} */.
- Include every file the code or sliprail.json refers to, such as icons and HTML pages. Hidden files and node_modules are not published.

## Code

main exports default an extension object:

```js
export default {
  shortcuts: [
    {
      id: 'count-words',
      displayName: { en: 'Count Words', 'zh-hans': '统计字数' },
      acceptsParameter: true,
      handle: async (context) => {
        const text = context.parameter || (await context.readTextFromClipboard());
        context.showNotification({ title: 'Words', message: String(text.split(/\s+/).filter(Boolean).length) });
      },
    },
  ],
};
```

- A shortcut has id, displayName and optionally description, icon, appendKeywords, acceptsParameter for text typed after the command, acceptsFiles, handle(context), and subShortcuts(context) returning a searchable list of shortcuts shown after the command.
- shortcuts can be a function of the context returning the list, and the extension can declare settings, read them with context.getSettings() and react in onReady(context).
- The context offers parameter, files, openUrl, openPath, writeTextToClipboard, readTextFromClipboard, showNotification, showConfirm, selectFiles, createStorage() for small key values, ai.request like apps when the person has AI credits, and createWindow({htmlFile, title, width, height, parameters, channel}) to show an HTML page of the package, which reads parameters with window.sliprail.getParameters() and talks to the extension through a channel from context.createWindowChannel().
- Keep handlers quick and asynchronous so the launcher stays responsive.

## Data

Keep what people add or save in data lists so it works offline and syncs to their other devices when cloud sync is on for the extension.

- Declare the lists in dataLists of sliprail.json like apps do: {"name": "notes", "description": "...", "keys": {"title": {"type": "string"}}}. Lists of extensions always belong to each person, so do not set scope.
- Fields can only be added later, never renamed, removed or retyped.
- Use context.data.lists() and context.data.list(name), which offer findAll(), get(id), count(), add(itemOrItems) resolving to the added records, update(itemOrItems) changing only the passed fields of each record id, remove(id) and removeMany(ids), all returning promises. Records also carry id, createdAt and updatedAt.

## Trying it

When Sliprail runs on this computer, make sure sliprail.json has an id, setting it to a new UUID if it has none, then open com.fengcen.sliprail://develop?path=<URL-encoded absolute path of the package directory> with open on macOS or Start-Process in PowerShell on Windows, and confirm in Sliprail. Open the link again after changes to reload it.

## Publishing

Publish with the tools of the Sliprail MCP server at https://sliprail.com/_api/sliprail/mcp. If they are not available, ask the user to connect it as described in https://sliprail.com/docs/developer/connect-your-agent; it needs an access token with Sliprail publishing access, created at https://account.fengcen.com/create-access-token.

1. Pack the package directory, the one with sliprail.json at its root, into a gzipped tarball outside of it, leaving out node_modules and hidden files:
   - macOS and Linux: `tar -czf "${TMPDIR:-/tmp}/sliprail-package.tgz" -C <package directory> --exclude=node_modules --exclude='*/.*' .`
   - Windows PowerShell: `tar.exe -czf "$env:TEMP\sliprail-package.tgz" -C <package directory> --exclude=node_modules --exclude="*/.*" .`
2. Read the size of the tarball in bytes with `wc -c < "${TMPDIR:-/tmp}/sliprail-package.tgz"`, or `(Get-Item "$env:TEMP\sliprail-package.tgz").Length` in PowerShell.
3. Call create_package_upload with the size, run the upload command it returns as it is, then call publish_package with the returned uploadKey.
4. When publish_package returns an id that is not the id in sliprail.json, or sliprail.json has no id, write the returned id into sliprail.json and keep it, so that later publishes update the same package and its data.

The extension stays private to the user and installs and updates automatically in their Sliprail desktop app after they sign in. To change a package published earlier, get its files with get_package_files, edit them and publish them again. Never include secrets or private data in the files.
