What plugins are
Plugins live in data/plugins/ (or DEGOOG_PLUGINS_DIR). The core also loads plugins from src/commands/builtins/ using the same folder structure and rules (see Contributing). Each plugin is a folder with an entry file. One plugin can expose several capabilities: a bang command, a slot, search bar actions, HTTP routes, and/or request middleware.
Folder structure
data/plugins/
my-plugin/
index.js # required — entry point
template.html # optional
style.css # optional
script.js # optional
The entry file must be index.js, index.ts, index.mjs, or index.cjs.
Asset files
- template.html — HTML fragment for your plugin output. Use
{{placeholders}}and replace them inexecute(). Injected into search results, not a full page. - style.css — Loaded and served when the plugin is active. Use class names scoped to your plugin to avoid conflicts.
- script.js — Client-side JavaScript, loaded automatically.
You can add other files and read them at runtime via ctx.readFile("filename") in init(ctx).
Plugin context and init()
If your plugin uses template.html, style.css, or script.js, the system loads them and passes a context to your optional init(context):
init(ctx) {
// ctx.template — contents of template.html (or "")
// ctx.dir — absolute path to the plugin folder
// ctx.readFile — async: ctx.readFile("my-data.json") reads from the plugin folder
}
init() runs once at startup, before configure().
Bang commands
Bang commands run when the user types !trigger something in the search bar. Export a single object (as export default or export const command) with:
- name (string) — Display name in Settings and
!help. - description (string) — Short description for
!help. - trigger (string) — The word after
!(e.g."weather"→!weather). - execute(args, context) (async) —
argsis the text after the trigger. Return aCommandResult.
CommandResult shape:
{ title: string, html: string, totalPages?: number }
CommandContext (second argument to execute):
{ clientIp?: string, page?: number }
Optional: aliases (string[] — extra triggers), naturalLanguagePhrases (string[] — phrases that trigger this command without ! when natural language is on in Settings), settingsSchema and configure(settings) for Settings → Plugins, init(ctx), isConfigured() (async, return false to hide from !help until configured).
SettingField (for settingsSchema):
{
key: string,
label: string,
type: "text" | "password" | "url" | "toggle" | "textarea",
required?: boolean,
placeholder?: string,
description?: string,
secret?: boolean // never sent to browser; server-side only
}
How settings work
- Declare
settingsSchema— a Configure button appears in Settings → Plugins. - User saves the form; values go to
data/plugin-settings.jsonunderplugin-<folderName>. configure(settings)is called after save and on server restart if settings exist.- Use
isConfigured()to return false when required settings are missing — the command is then hidden from!help. - Users can disable any plugin with the toggle in Settings → Plugins; disabled plugins are hidden from
!helpand return an error when invoked.
Examples from the official store:
- Weather — bang command with template, styles, and settings (default city, Fahrenheit).
Slot plugins
Slots inject panels into the search results page when the query matches. Export slot or slotPlugin (same module can also export a bang command):
- id, name — Unique id and display name.
- position —
"above-results","below-results","sidebar", or"at-a-glance". - trigger(query) — Return true (or a Promise that resolves true) if the slot should show for this query.
- execute(query, context) (async) — Returns
{ title?: string, html: string }.contexthasclientIp?and, forat-a-glanceslots only,results?(the search results array).
Optional: settingsId (string) — Key under which settings are stored in plugin-settings.json. Default is slot-<id>. Use settingsId to keep an existing settings key (e.g. settingsId: "ai-summary").
Optional: settingsSchema, configure(settings), init(ctx). Multiple slots can match the same query; all are shown.
At a glance slot
Slots with position: "at-a-glance" fill the At a glance block above the search results (the snippet that would otherwise show the top result). Use this for summaries, AI-generated blurbs, or other content that benefits from the search result list.
Behaviour: The search response is not delayed by at-a-glance slots. The client shows results immediately and fetches at-a-glance content in a separate request (POST /api/slots/glance with body { query, results }). Your execute(query, context) receives context.results so you can use the result list (e.g. for an AI summary). Until the glance request returns, an animated placeholder is shown; if no panel is returned (e.g. disabled or error), the default “first result” glance is shown.
Example (built-in): AI Summary is a slot in src/commands/builtins/ai-summary/ with position: "at-a-glance" and settingsId: "ai-summary". When enabled, it calls an OpenAI-compatible API with the query and result snippets and replaces the glance with a short AI summary.
Example from the official store: TMDb (imdb-slot) — slot plugin that shows movie/TV details above search results.
Slot API: GET /api/slots?q=<query> returns panels for above-results, below-results, and sidebar (at-a-glance slots are excluded so the search response is fast). POST /api/slots/glance with body { query, results } returns only at-a-glance panels; the client calls this after the search response to fill the glance without blocking results.
Search bar actions
Plugins can add buttons or links next to the search bar (home and results). Export searchBarActions — an array of objects with this shape:
- id (string) — Unique id for the action.
- label (string) — Button text. Can be overridden by plugin settings: if your plugin has a
buttonLabel(or similar) key insettingsSchema, that value is used when the user configures it. - type —
"navigate"(open URL),"bang"(fill bar with!trigger), or"custom"(yourscript.jslistens forsearch-bar-actionevent). - For
navigate: url. Forbang: trigger. Optional: icon (image URL).
For custom, the core dispatches a search-bar-action CustomEvent with detail: { actionId, inputId, input }. Your script can open a modal, file picker, or call your plugin route.
Plugin-registered routes
Plugins can expose HTTP API under /api/plugin/<folderName>/.... Export routes — array of:
{ method: "get"|"post"|"put"|"delete"|"patch", path: string, handler(req) }
path is the segment after the plugin id (e.g. "search" → /api/plugin/my-plugin/search). handler receives the standard Request and returns Response or Promise<Response>. Use for custom APIs, callbacks, or serving HTML/JSON.
Request middleware
Middleware lets a plugin hook into specific request flows. Export middleware — object with id, name, and handle(req, context). context can include { route: "settings-auth" } etc. Return:
Response— the core returns it to the client.{ redirect: url }— the core may redirect (e.g. on settings callback it also issues a session token and appends it to the redirect URL).null— the core continues with default behaviour.
The core uses middleware for the settings gate (who can open Settings). To select a plugin for the gate, use the “Use as settings gate” toggle in that plugin’s Configure in Settings → Plugins; that sets middleware.settingsGate in plugin-settings.json to plugin:<folderName>.
Settings gate (login flow)
By default, Settings can be protected with a password (DEGOOG_SETTINGS_PASSWORDS). Alternatively, a middleware plugin can be the settings gate: when the user opens Settings, they are sent through your plugin’s flow (e.g. OIDC, magic link), then back to Settings with a session token.
How to enable it
- Add a plugin that exports
middlewareand implements the settings-auth routes below. - In Settings → Plugins, open Configure for that plugin.
- Turn on “Use as settings gate” and save.
What your middleware must do
- route "settings-auth" — Return a JSON response with
required: true,valid: false, andloginUrl: "https://..."(e.g. your plugin’s login page). - route "settings-auth-callback" — After the user has authenticated, return
{ redirect: "/settings" }. The core issues a session token and redirects the user to that URL with?token=.... - route "settings-auth-post" — If you don’t support password-style POST, return a 400 or similar
Response.
Your plugin can expose a GET /login route that shows a page and redirects the browser to /api/settings/auth/callback?returnTo=/settings. The core then attaches the token and sends the user to Settings.
How to verify it works
Clear the settings token: DevTools → Application → Session Storage → remove degoog-settings-token. Open Settings. You should be redirected to your plugin’s login, then back to the full Settings page.
Example: minimal plugin with template
Folder: data/plugins/greeting/ with index.js, template.html, style.css.
index.js
let template = "";
export default {
name: "Greeting",
description: "Say hello",
trigger: "hello",
init(ctx) {
template = ctx.template;
},
async execute(args) {
const name = args.trim() || "world";
const html = template.replace("{{name}}", name);
return { title: "Hello", html };
},
};
template.html
<div class="command-result greeting">
<h3 class="greeting-title">Hello, {{name}}!</h3>
</div>
style.css
.greeting-title {
color: var(--text-primary);
font-size: 1.5rem;
}