Calling OpenClacky's Native APIs
Many features users ask for are already natively supported by the OpenClacky main service — sessions, trash, skills, memories, cron tasks, billing, media generation… each has a ready-made HTTP endpoint. An extension panel doesn't need to rebuild these capabilities — it can call the main service's endpoints directly.
A typical case: an extension with a delete feature that wants deleted files to be recoverable from file recovery. There's no need to implement a separate trash system — hand the file to OpenClacky's trash on delete, and call
POST /api/trash/restoreto restore it, and the file appears in the official file-recovery UI. When a need arises, check here first for a ready-made endpoint.
How to call: the panel shares the host's origin
A Web UI panel (view.js) and the official OpenClacky interface run on the same page, the same origin. So a panel can fetch("/api/...") the main service directly, and the browser carries the access key automatically — no extra handling needed:
// Inside a panel's view.js, fetch a main-service endpoint directly
const res = await fetch("/api/skills");
const data = await res.json();
console.log("installed skills:", data.skills);
This section covers the frontend panel calling main-service endpoints. An extension's backend
handler.rbshould notfetchthese endpoints — to drive sessions from the backend, use the white-listed methods (create_session/submit_task/dispatch_to_session, see HTTP API Extensions). The endpoints listed here are for the panel frontend.
All endpoints live under /api/ and return JSON. Below are the endpoints extensions may safely call, grouped by capability.
Sessions
| Method | Path | Purpose |
|---|---|---|
| GET | /api/sessions |
List all sessions |
| GET | /api/sessions/:id |
A single session's detail |
| GET | /api/sessions/:id/messages |
The session's message history |
| GET | /api/sessions/:id/files |
Files in the session's working directory |
| GET | /api/sessions/:id/git/:action |
The session's git status/diff, etc. |
| GET | /api/sessions/:id/time_machine |
The session's time-machine snapshots |
| POST | /api/sessions |
Create a session |
| PATCH | /api/sessions/:id/model |
Switch the session's model |
| PATCH | /api/sessions/:id/working_dir |
Change the session's working directory |
Create a session POST /api/sessions:
const res = await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "My new session", // required
agent_profile: "general", // optional, defaults to general
working_dir: "/path/to/dir", // optional, defaults to current working dir
model_id: "…" // optional, a model id from GET /api/config
}),
});
const { session } = await res.json(); // 201 → { session: {...} }
PATCH /api/sessions/:id/model and /working_dir mutate a running session — these are operations with side effects. Call them only when the user explicitly asks; they should not silently alter a session.
Trash (file recovery)
Deleting files through OpenClacky's trash makes them restorable from the official file recovery UI — exactly the path for "an extension that deletes, with restore support."
| Method | Path | Purpose |
|---|---|---|
| GET | /api/trash |
List files in the trash (add ?project=<path> to filter a project) |
| POST | /api/trash/restore |
Restore a single file to its original location |
| GET | /api/trash/sessions |
List sessions in the trash |
| POST | /api/trash/sessions/restore |
Restore a deleted session |
Restore a file POST /api/trash/restore:
await fetch("/api/trash/restore", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project_root: "/path/to/project", // project root the file belongs to
original_path: "/path/to/project/notes.md", // the file's original path
}),
});
// 200 → { ok: true, restored_file, message }
// If a file already exists at the original location, returns 422 — no overwrite
GET /api/trash returns:
{
"ok": true,
"files": [
{ "original_path": "…", "file_size": 1234, "deleted_at": "…",
"project_root": "…", "project_name": "…" }
],
"projects": [ { "project_root": "…", "file_count": 3, "total_size": 4567 } ],
"total_count": 3
}
Skills, Agents, Channels, MCP
| Method | Path | Purpose |
|---|---|---|
| GET | /api/skills |
Installed skills |
| GET | /api/agents |
Available agents |
| GET | /api/sessions/:id/skills |
Skills available in a session |
| GET | /api/agents/:id/skills |
Skills bound to an agent |
| GET | /api/providers |
Available models / providers |
| GET | /api/channels |
Configured IM channels |
| GET | /api/mcp |
Configured MCP servers |
All read-only — suited for "what capabilities are installed" overview panels.
Memories
| Method | Path | Purpose |
|---|---|---|
| GET | /api/memories |
List long-term memories |
| POST | /api/memories |
Write a memory |
Write a memory POST /api/memories:
await fetch("/api/memories", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: "my-note.md", // must end in .md, no path separators
content: "# Title\nBody…",
}),
});
// 201 → { ok: true, memory: {...} }
// Already exists → 409
Cron tasks
| Method | Path | Purpose |
|---|---|---|
| GET | /api/cron-tasks |
List cron tasks |
| POST | /api/cron-tasks |
Create a cron task |
Create a cron task POST /api/cron-tasks:
await fetch("/api/cron-tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Daily briefing", // required
content: "Summarize today's news…", // required, the task prompt
cron: "0 9 * * *", // required, standard 5-field cron
enabled: true, // optional, defaults to true
}),
});
// 201 → { ok: true, name }
Billing / usage
| Method | Path | Purpose |
|---|---|---|
| GET | /api/billing/summary |
Usage summary |
| GET | /api/billing/daily |
Usage by day |
| GET | /api/billing/records |
Detailed records |
| GET | /api/billing/sessions |
Usage by session |
Suited for "how much did I spend this month" usage panels — all read-only.
Files
| Method | Path | Purpose |
|---|---|---|
| POST | /api/upload |
Upload a file (multipart, field name file) |
| POST | /api/file-action |
Open / download a file on the local machine |
Open a file POST /api/file-action:
await fetch("/api/file-action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: "/path/to/file.pdf",
action: "open", // "open" default app | "reveal" show in file manager | "download"
}),
});
Media generation (billed)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/media/image |
Generate an image |
| POST | /api/media/video |
Generate a video |
| POST | /api/media/audio/speech |
Text-to-speech |
These endpoints incur real charges. They should be called only on an explicit user action (e.g. clicking a "Generate" button) — not automatically inside a panel's
render, and not in a loop, or they will keep burning the user's quota.
A full example: a delete panel with trash restore
// panels/trash-tool/view.js
Clacky.ext.ui.mount("session.aside", (container, ctx) => {
const btn = document.createElement("button");
btn.className = "btn-primary"; // reuse the official theme
btn.textContent = "Restore last deleted file";
btn.addEventListener("click", async () => {
// 1. Look up the most recently trashed file
const trash = await (await fetch("/api/trash")).json();
const latest = trash.files[0];
if (!latest) return Clacky.Notify.info("Trash is empty");
// 2. Restore it
const res = await fetch("/api/trash/restore", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project_root: latest.project_root,
original_path: latest.original_path,
}),
});
const out = await res.json();
Clacky.Notify.info(out.ok ? "Restored" : out.error);
});
container.appendChild(btn);
}, { tab: { id: "trash-tool", label: () => "Trash" } });
When deleting a file, have the extension's delete logic move the file into OpenClacky's trash (instead of a raw rm), and it can be restored from the official file-recovery UI or from the panel — no wheel-reinventing.
Boundaries: what's not here
This page lists endpoints extensions may safely call. Destructive, system-level, or authorization endpoints (permanent deletion, changing global config, restarting the server, brand licensing, installing/disabling extensions, etc.) are not open to extensions — they belong to the main framework's own control plane. If a need has no matching endpoint here, it likely falls outside an extension's scope — rethink the approach, or implement it in your own backend via HTTP API Extensions.