Skip to content

For developers

Teach Diurna a new source.

A plugin is a small TypeScript module: one definePlugin call, an honest manifest, a single-file bundle. If your tool knows something about your workday, it can be a signal source.

developers.md →

Paste it into Claude — or any agent — and describe your source. It carries the full contract: signal shape, SDK hooks, manifest schema, bundling, submission.

1 Write the plugin

Install @diurna/plugin-sdk and default-export a definePlugin. There are two optional hooks, and you implement whichever fit:

  • provide(settings, day) — a Provider: a pure function that returns one day's signals from a durable source. The engine's cache-through wrapper handles storage — past days are served from the DB, today is always re-provided.
  • watch(ctx, settings) — a Watcher: for signals that must be captured the moment they happen. Called on a cadence; acts only through the typed ctx (readSignals, emitPrompt, getOAuthToken) — it never draws UI and never writes the DB.
import { definePlugin } from '@diurna/plugin-sdk'

export default definePlugin({
  id: 'acme.standup',
  settings: {
    apiHost: { default: 'api.acme.dev', kind: 'str', help: 'Standup API host' },
  },
  // A Provider: "give me this day's signals." Pure — never touches the DB.
  provide: async (settings, day) => {
    const entries = await fetchStandups(settings.apiHost, day)
    return entries.map((e) => ({
      kind: 'standup',
      tsStart: e.postedAt,
      data: { text: e.text, channel: e.channel },
    }))
  },
})

Settings descriptors (default / kind / help) drive a generated settings pane — plugins never ship UI.

2 Declare the manifest

manifest.json is hand-authored, deliberately: it's the security contract a reviewer reads as a static declaration, never inferred from code. Declare only what you need — the capability sheet is what users consent to, and an update asking for more is held for re-consent.

{
  "id": "acme.standup",
  "name": "Acme Standup",
  "version": "1.0.0",
  "entry": "dist/bundle.cjs",
  "author": "acme",
  "license": "MIT",
  "repo": "https://github.com/acme/diurna-standup",
  "capabilities": {
    "network": { "hosts": ["api.acme.dev"] }
  },
  "provides": [
    {
      "kind": "standup",
      "point": true,
      "description": "a standup note the user posted; data.text, data.channel."
    }
  ]
}

The one duplication — manifest.provides vs the code's signal descriptors — is checked at build time: the build fails on drift, so the reviewed declaration and the shipped behavior can't diverge.

3 Bundle to one file

Plugins ship as a single-file JS artifact with dependencies baked in at publish time — no dependency resolution ever runs on a user's machine. The bundle runs in an isolated subprocess, sandboxed to exactly the capabilities the manifest declares.

4 Submit by pull request

The registry is a git repo with no backend. Listing means opening a PR that adds your entry — repo, pinned version, bundle checksum, embedded manifest — and CI validates it with static analysis only (it never runs your code):

  • Manifest matches the schema; entry key equals manifest.id
  • License is OSI-approved — open source is required for listing
  • The bundle at your release URL matches the pinned sha256
  • The embedded manifest byte-matches the one in the pinned bundle

Pinning is the security property: every update goes through re-review and a new pinned checksum, so a compromised author account can't silently push code to users.