Plugin Development Workflow
Plugin Development Workflow
The shortest stable path from a CoreBox command to a publishable plugin package.
Scope
Use this workflow for index.js Prelude plugins that declare CoreBox features, return search results, run item actions, then build a .tpex package with TUFF CLI and publish it to Nexus.
If the plugin needs a heavier Vue/React UI, keep the same flow: Manifest declares capabilities, index.js performs lightweight registration and routing, and Surface/UI loads only when needed.
1. Manifest
New plugins should use main: "index.js" and declare the latest sdkapi, category, permissions, and permission reasons.
{
"id": "com.example.quick-note",
"name": "quick-note",
"version": "0.1.0",
"author": "Example",
"sdkapi": 260626,
"category": "productivity",
"description": "Save selected text as a quick note.",
"main": "index.js",
"permissions": {
"required": ["clipboard.read"],
"optional": ["clipboard.write"]
},
"permissionReasons": {
"clipboard.read": "Read clipboard text as note content",
"clipboard.write": "Copy processed note content back to the clipboard"
},
"features": [
{
"id": "quick-note.save",
"name": "Save Quick Note",
"desc": "Save input or clipboard text as a note",
"keywords": ["note", "memo"],
"push": true,
"acceptedInputTypes": ["text"],
"commands": [
{ "type": "over", "value": ["note", "memo"] }
]
}
]
}
Checklist:
sdkapi >= 260114requirescategory.- Keep
permissions.requiredlimited to startup or core-path requirements. - Add
permissionReasonsfor every non-auto-granted permission so users understand the request.
2. Prelude
index.js is the plugin Prelude. It runs in a Node.js sandbox and reads plugin APIs from globalThis.
const { clipboard, logger, box, TuffItemBuilder } = globalThis
const PLUGIN_NAME = 'quick-note'
const COPY_ACTION_ID = 'copy'
function getQueryText(query) {
return typeof query === 'string' ? query : query?.text ?? ''
}
function buildCopyItem(text) {
return new TuffItemBuilder('quick-note.copy')
.setSource('plugin', 'plugin-features', PLUGIN_NAME)
.setTitle('Copy note content')
.setSubtitle(text.slice(0, 80))
.setMeta({
pluginName: PLUGIN_NAME,
defaultAction: COPY_ACTION_ID,
})
.createAndAddAction(COPY_ACTION_ID, 'copy', 'Copy', text)
.build()
}
module.exports = {
async onFeatureTriggered(featureId, query, feature, signal) {
const text = getQueryText(query).trim()
if (!text) {
return []
}
logger.info('feature triggered', { featureId, featureName: feature?.name })
return [buildCopyItem(text)]
},
async onItemAction(item) {
const action = item.actions?.find(action => action.id === COPY_ACTION_ID || action.type === 'copy')
if (action?.payload) {
clipboard.writeText(action.payload)
}
box?.hide?.()
},
## }
Checklist:
- Do not use the legacy
init(ctx)model for new plugins. - Treat
queryas either a string or aTuffQueryobject. - Build results with
TuffItemBuilderand includepluginNameplus the default action inmeta. - Long-running search, network, or compute work should respect
signalso CoreBox can cancel stale requests.
3. Pick SDKs
Choose the smallest SDK for the task. Do not request permissions for future features.
| Task | Recommended entry | Notes |
|---|---|---|
| CoreBox results | TuffItemBuilder, Feature SDK | Build items directly in Prelude; use Feature SDK for more complex dynamic lists. |
| Clipboard | clipboard or useClipboard() | Clipboard write is low risk; clipboard read requires clipboard.read and should be requested only when needed. |
| Plugin settings | storage or usePluginStorage() | Use for regular settings and non-sensitive data; each plugin has a storage quota. |
| Structured plugin data | usePluginSqlite() | Requires sdkapi >= 260215 and storage.sqlite. |
| Secrets | usePluginSecret() | API keys, tokens, and provider secrets must not be stored in plain JSON, localStorage, or logs. |
| Private user sync | CloudSyncSDK | Uses /api/v1/sync/*; sync payloads must be encrypted through payload_enc or payload_ref. |
| Content sharing | CloudShareSDK | Publishes public or team-visible plugin content packages, such as snippet packs; do not use it as private sync. |
| Transport | typed transport SDK | New capabilities should use typed transport instead of adding raw IPC dependencies. |
| AI capability / command | intelligence | Declare intelligence.basic, discover capability health first, and pass per-command templates through typed invoke options rather than raw IPC. |
For a Raycast-style AI Command, keep the command definition in the plugin and let the host own provider selection, audit, quota, and fallback:
const { intelligence } = globalThis
async function runRewriteCommand(text, tone) {
const status = await intelligence.getCapabilityStatus({ capabilityId: 'text.chat' })
if (!status.available) throw new Error(status.reason || 'AI unavailable')
return intelligence.text.chat(
{ messages: [{ role: 'user', content: text }] },
{
promptTemplate: 'Rewrite the input in a {{tone}} tone. Return only the rewritten text.',
promptVariables: { tone },
},
)
## }
Do not store provider credentials in the command, prompt variables, plugin storage, or logs. See Intelligence SDK for the typed contract.
4. Secure Plugin Views
Plugin BrowserWindow and WebContentsView surfaces require sdkapi >= 260615 and always run with the bundled Tuff preload. The renderer receives only frozen $plugin, $config, and $channel globals; use $plugin.bridgeVersion for bridge capability detection.
Custom preload scripts, <webview>, renderer require / process / Electron access, production remote URLs, popups, and downloads are unsupported. Remove those dependencies before raising the SDK marker. Incompatible surfaces fail before window creation with PLUGIN_WINDOW_LEGACY_RUNTIME_UNSUPPORTED; there is no environment compatibility override.
5. Validate
Run these local checks before publishing:
tuff validate --strict
tuff build
tuff publish --dry-run
Repository changes should also include nearest-path validation such as focused Vitest, file-level ESLint, and git diff --check. Do not report unrelated historical lint noise as a failure of the current plugin change.
6. Build And Publish
Standard publish path:
tuff login
tuff validate --strict
tuff build
tuff publish --dry-run
tuff publish --tag 0.1.0 --channel BETA
Publishing notes:
tuff buildcreatesdist/build/and the.tpexplugin package.tuff publish --dry-runpreviews locally without uploading.- Nexus API keys need at least
plugin:publish; publisher preflight validates publisher access, and the publish scope covers the plugin read needed before upload. - If Nexus rejects the CLI token, run
tuff loginto refresh browser auth, or check API key scopes.
7. Plugin Packages Vs Content Packages
Plugin package publishing and plugin content package publishing are separate:
- Plugin package: publishes
.tpex, updating plugin code, Manifest, assets, and Surface. - Content package: publishes installable data for a plugin, such as
touch-snippetstuff.snippet-pack+json. - CloudSync: syncs a user's own encrypted business data and is not for public store content.
- CloudShare: distributes installable content packages after sensitive-data filtering and target-plugin validation.
When a plugin supports both sync and sharing, keep sync data, share data, and import/merge logic separate so private user data is not accidentally published as public content.