Getting Started

In this tutorial we’ll build a Session Timer plugin — a simple timer extension that tracks session and encounter time. By the end, you’ll have a working plugin loaded in Overseer and know how to publish it to the marketplace.

Prerequisites: Node.js, npm, and Overseer installed.

Step 1: Find the Plugins directory

Open Overseer and go to Settings > Data and Storage. The Data directory path is shown at the top.

Default locations:

PlatformPath
macOS~/Library/Application Support/Overseer Studio/
Windows%APPDATA%\Overseer Studio\
Linux~/.config/Overseer Studio/

Open that directory and look for a Plugins/ subdirectory. Create it if it doesn’t exist.

Step 2: Create your plugin directory

Inside Plugins/, create a directory for your plugin. Scoped directories keep plugins organized:

Plugins/@yourname/session-timer/

Replace yourname with a handle that will become your scope in step 4.

Open that directory in your terminal for the rest of the tutorial.

Step 3: Set up the project

Initialize npm and install the SDK:

npm init -y
npm install --save-dev @overseer-studio/sdk

Add the overseer CLI to your npm scripts in package.json:

{
  "scripts": {
    "overseer": "overseer"
  }
}

Step 4: Log in and claim a scope

Log in to the marketplace:

npm run overseer -- login

Then claim your scope. A scope is a namespace prefix (@yourname) that makes your plugin IDs unique across all developers:

npm run overseer -- scope claim @yourname

Step 5: Initialize the plugin

npm run overseer -- init

The prompt will show your claimed scope and ask for a plugin name and display details. After the prompts, it creates:

  • manifest.json — the plugin manifest
  • extensions/ — directory for your extensions
  • presets/ — directory for presets (unused today)

Your directory should look like this:

@yourname/session-timer/
  package.json
  manifest.json
  extensions/
  presets/

Step 6: Create the extension

An extension is the tile content — in this case, the timer UI. Create the extension directory and its files:

extensions/session-timer/
  manifest.json
  index.html
  assets/
    icon.png

extensions/session-timer/manifest.json

{
  "id": "@yourname/session-timer",
  "label": "Session Timer",
  "description": "Track session and encounter time.",
  "category": "Organization",
  "version": "0.1.0",
  "icon": { "$ref": "assets/icon.png" },
  "source": { "$ref": "index.html" }
}

extensions/session-timer/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Session Timer</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: system-ui, sans-serif;
      background: transparent;
      color: #e0e0e0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      gap: 20px;
    }
    #display {
      font-size: 3rem;
      font-variant-numeric: tabular-nums;
      letter-spacing: 0.05em;
    }
    .controls { display: flex; gap: 8px; }
    button {
      background: #3f3f46;
      color: #fafafa;
      border: none;
      padding: 10px 20px;
      border-radius: 6px;
      font-size: 0.9rem;
      cursor: pointer;
    }
    button:hover { background: #52525b; }
  </style>
</head>
<body>
  <div id="display">00:00:00</div>
  <div class="controls">
    <button id="toggle">Start</button>
    <button id="reset">Reset</button>
  </div>

  <script>
    let elapsed = 0;
    let timer = null;

    const display = document.getElementById('display');
    const toggleBtn = document.getElementById('toggle');
    const resetBtn = document.getElementById('reset');

    function format(s) {
      const h = String(Math.floor(s / 3600)).padStart(2, '0');
      const m = String(Math.floor(s % 3600 / 60)).padStart(2, '0');
      const sec = String(s % 60).padStart(2, '0');
      return `${h}:${m}:${sec}`;
    }

    toggleBtn.addEventListener('click', () => {
      if (timer) {
        clearInterval(timer);
        timer = null;
        toggleBtn.textContent = 'Start';
      } else {
        timer = setInterval(() => {
          elapsed++;
          display.textContent = format(elapsed);
        }, 1000);
        toggleBtn.textContent = 'Pause';
      }
    });

    resetBtn.addEventListener('click', () => {
      clearInterval(timer);
      timer = null;
      elapsed = 0;
      display.textContent = format(0);
      toggleBtn.textContent = 'Start';
    });
  </script>
</body>
</html>

assets/icon.png

Add a square PNG icon (at least 64×64 pixels) at extensions/session-timer/assets/icon.png. This appears in the tile picker.

Step 7: Update the plugin manifest

Open manifest.json (the plugin manifest at the root) and add a reference to the extension you just created:

{
  "id": "@yourname/session-timer",
  "name": "Session Timer",
  "version": "0.1.0",
  "author": "Your Name",
  "extensions": [
    { "$ref": "extensions/session-timer/manifest.json" }
  ],
  "presets": [],
  "themes": [],
  "locales": []
}

Step 8: Test it

Restart Overseer. Create a tile, open the extension picker, and look under Organization. “Session Timer” should appear with your icon.

Select it. The timer loads inside the tile. Click Start to begin counting.

Plugin changes are picked up at startup. Restart Overseer whenever you change a manifest or replace asset files.

Troubleshooting

If the extension doesn’t appear:

  • Validate your JSON — a missing comma or bracket will silently fail. Use a JSON validator.
  • Check the icon path — the file referenced by icon must exist at the exact path relative to the extension manifest.
  • Restart Overseer — plugins load at startup only.

Step 9: Publish (optional)

Publishing makes your plugin available to others in the marketplace. It’s not required to use the plugin yourself — it’s already working locally.

Build the plugin:

npm run overseer -- build

Then publish:

npm run overseer -- publish

That’s it. When it finishes, you’ll see the public URL for your plugin.

For more detail on the publishing workflow — versioning, tokens, CI — see Publishing a Plugin.

What’s next