Getting Started
Build your first CloudCLI UI plugin from scratch — a working tab plugin in under 10 minutes.
This guide walks you through building a complete plugin from scratch. By the end, you'll have a working tab plugin with a frontend UI and a backend server.
An example of a plugin-starter can be found on https://github.com/cloudcli-ai/cloudcli-plugin-starter
Prerequisites
- A running CloudCLI UI instance
- Node.js 18+ installed
- Git installed
- A GitHub (or any Git host) account for publishing
Step 1: Create the Plugin Directory
mkdir my-first-plugin
cd my-first-plugin
git initStep 2: Create the Manifest
Create manifest.json — this tells CloudCLI UI about your plugin:
{
"name": "my-first-plugin",
"displayName": "My First Plugin",
"version": "1.0.0",
"description": "A simple plugin that shows project info and a greeting.",
"author": "Your Name",
"icon": "Zap",
"type": "module",
"slot": "tab",
"entry": "index.js",
"server": "server.js"
}The key fields:
- name — Unique identifier. Only letters, numbers, hyphens, and underscores.
- entry — Your frontend JavaScript file.
- server — Your backend Node.js file (optional — remove this line if you don't need a backend).
For all fields, see the Manifest Reference.
Step 3: Write the Frontend Module
Create index.js. Your plugin needs to export two functions: mount and unmount.
// index.js
export function mount(container, api) {
// 1. Read current context
const ctx = api.context;
// 2. Render UI
container.innerHTML = `
<div style="padding: 24px; font-family: system-ui, sans-serif;">
<h1 style="margin: 0 0 8px;">Hello from My First Plugin!</h1>
<p style="color: #888;">Theme: ${ctx.theme}</p>
<p style="color: #888;">Project: ${ctx.project?.name ?? 'None selected'}</p>
<div id="server-data" style="margin-top: 16px;">Loading server data...</div>
</div>
`;
// 3. Fetch data from your backend server
api.rpc('GET', '/hello')
.then(data => {
const el = container.querySelector('#server-data');
if (el) el.textContent = `Server says: ${data.message}`;
})
.catch(err => {
const el = container.querySelector('#server-data');
if (el) el.textContent = `Server error: ${err.message}`;
});
// 4. Subscribe to context changes (theme, project, session)
const unsubscribe = api.onContextChange((newCtx) => {
const h1 = container.querySelector('h1');
if (h1) {
h1.style.color = newCtx.theme === 'dark' ? '#fff' : '#000';
}
});
// Store unsubscribe for cleanup
container._cleanup = unsubscribe;
}
export function unmount(container) {
if (container._cleanup) {
container._cleanup();
}
container.innerHTML = '';
}What's happening:
mount(container, api)is called when the plugin tab opens. You get a DOM element to render into and anapiobject.api.contextgives you the current theme, project, and session.api.rpc(method, path, body?)calls your backend server through the host's proxy.api.onContextChange(callback)notifies you when context changes. Returns an unsubscribe function.unmount(container)is called when the tab closes — clean up here.
For the full API, see the Frontend API Reference.
Step 4: Write the Backend Server
Create server.js. Your server must listen on a random port and print a JSON ready signal to stdout.
// server.js
import http from 'node:http';
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
res.setHeader('Content-Type', 'application/json');
if (url.pathname === '/hello') {
res.writeHead(200);
res.end(JSON.stringify({
message: `Hello from the plugin server! (Node ${process.version})`,
pluginName: process.env.PLUGIN_NAME
}));
return;
}
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
});
// Listen on a random port, bind to localhost only
server.listen(0, '127.0.0.1', () => {
const port = server.address().port;
// CRITICAL: Print the ready signal. The host waits for this.
console.log(JSON.stringify({ ready: true, port }));
});Three rules for your server:
- Listen on port 0 — the OS assigns a random available port
- Bind to 127.0.0.1 — never expose to the network
- Print the ready signal —
{"ready": true, "port": <number>}as a JSON line to stdout within 10 seconds
For advanced patterns (Express, secrets, async init, caching), see Backend Servers.
Step 5: Test Locally (Optional)
You can test your server standalone:
node server.js
# Should print: {"ready":true,"port":XXXXX}
# In another terminal:
curl http://127.0.0.1:XXXXX/helloStep 6: Publish and Install
Push your plugin to a Git repository:
git add .
git commit -m "Initial plugin"
git remote add origin https://github.com/yourname/my-first-plugin.git
git push -u origin mainThen install it in CloudCLI UI:
- Open Settings (gear icon in the sidebar)
- Go to the Plugins tab
- Paste your Git URL:
https://github.com/yourname/my-first-plugin.git - Click Install
Your plugin tab should appear immediately in the main content area.
Step 7: Iterate
To update your plugin after making changes:
- Push changes to your Git repo
- In Settings → Plugins, click the refresh icon next to your plugin
- The host pulls the latest code and restarts your server
Frontend-Only Plugin
If you don't need a backend server, remove the server field from your manifest:
{
"name": "simple-widget",
"displayName": "Simple Widget",
"version": "1.0.0",
"type": "module",
"slot": "tab",
"entry": "index.js"
}Your mount function can still use api.context and api.onContextChange — you just won't have api.rpc calls.
Next Steps
- Manifest Reference — Every field in
manifest.jsonexplained - Frontend API Reference — Full documentation of the
apiobject - Backend Servers — Advanced server patterns, secrets, and lifecycle
- Security Model — How plugins are isolated
- Example: Project Stats — Walk through the built-in example plugin