This is the public SDK for building tokentop plugins. It provides type definitions, helper utilities, and a test harness that community developers use to create providers, agents, themes, and notification plugins for tokentop.
This package is a dependency of community plugins, not of the main app. The main tokentop app implements these interfaces internally; this package exists so plugin authors get types, autocomplete, and testing utilities without importing from tokentop's internals.
- Runtime: Bun
- Language: TypeScript (strict mode)
- Package Manager: Bun
- Build:
tscfor declarations +bun buildfor JS output
The SDK is primarily type definitions. The interfaces in src/types/ define the plugin API contract. Community plugins depend on these types; breaking them is a major version bump.
This package must NEVER import from the tokentop app (tokentop/src/*). The SDK is a standalone package. Core implements these interfaces; the SDK only defines them.
The v2 plugin architecture moves credential discovery INTO plugins. Plugins implement auth.discover(ctx) using helpers from ctx.authSources. The old pattern of declaring auth: { envVars, externalPaths } and letting core run centralized discovery is deprecated.
Plugins never construct their own HTTP clients, loggers, or storage. Core creates these (with sandboxing/permissions) and injects them via context objects. The SDK defines the context interfaces so plugins can type their method signatures.
src/
├── index.ts # Main entry — re-exports everything
├── types/
│ ├── index.ts # Barrel export for all types
│ ├── plugin.ts # BasePlugin, PluginId, PluginPermissions, ConfigField, PluginLogger
│ ├── context.ts # PluginContext, AuthSources, PluginHttpClient, PluginStorage
│ ├── provider.ts # ProviderPlugin, Credentials, ProviderUsageData, ProviderAuth
│ ├── agent.ts # AgentPlugin, SessionUsageData, ActivityCallback
│ ├── theme.ts # ThemePlugin, ThemeColors, ThemeComponents
│ └── notification.ts # NotificationPlugin, NotificationEvent
├── helpers/
│ ├── index.ts # Barrel export for helpers
│ ├── create.ts # createProviderPlugin(), createAgentPlugin(), etc.
│ ├── auth.ts # Credential construction helpers (apiKeyCredential, oauthCredential, etc.)
│ └── version.ts # SDK_VERSION, CURRENT_API_VERSION, compatibility checks
└── testing/
├── index.ts # Barrel export for test utilities
└── harness.ts # createTestContext(), mock factories for logger/http/storage
| Type | Interface | Purpose | Key Methods |
|---|---|---|---|
provider |
ProviderPlugin |
Fetch usage data from AI model providers | auth.discover(), auth.isConfigured(), fetchUsage() |
agent |
AgentPlugin |
Parse coding agent sessions for token tracking | isInstalled(), parseSessions(), startActivityWatch?() |
theme |
ThemePlugin |
Color schemes for the TUI | Pure data: theme.colors, theme.components |
notification |
NotificationPlugin |
Alert delivery (Slack, Discord, terminal, etc.) | initialize(), notify(), test?(), supports?() |
Two version numbers matter:
| Version | What | Who Bumps It |
|---|---|---|
SDK semver (package.json version) |
npm package version | Normal semver rules |
apiVersion (integer, currently 2) |
Plugin contract version | Bumped rarely, only for breaking contract changes |
apiVersionis checked by core at plugin load time- SDK semver follows normal rules: patch for fixes, minor for additions, major for breaking type changes
- Both must be considered: a new optional field on
ProviderPluginis a minor SDK bump but does NOT bumpapiVersion
- Adding a required method to a plugin interface
- Changing the signature of an existing required method
- Removing a context field that plugins depend on
- Restructuring the plugin shape (e.g. moving
authfrom declarative to method-based)
- Adding optional methods or fields
- Adding new types/interfaces
- Adding helper functions
- Expanding union types (e.g. new
NotificationEventType)
All plugin types share these optional lifecycle hooks (from BasePlugin):
load → validate → initialize() → start() → [runtime] → stop() → destroy()
| Hook | When Called | Use Case |
|---|---|---|
initialize() |
After load + validation | Open connections, allocate resources |
start() |
After init, on app start or re-enable | Begin polling, start watchers |
stop() |
Before destroy, on disable | Pause work, stop watchers |
destroy() |
Before unload, on app shutdown | Close connections, flush buffers |
onConfigChange() |
When user changes plugin settings | React to config updates |
The key architectural decision: plugins own their credential discovery.
- Core creates a
PluginContextwith sandboxedauthSourceshelpers - Core calls
plugin.auth.discover(ctx) - Plugin uses
ctx.authSources.env.get(),ctx.authSources.files.readJson(),ctx.authSources.opencode.getProviderEntry()to find credentials - Plugin returns
CredentialResult— either{ ok: true, credentials }or{ ok: false, reason, message } - Core never needs to know which env vars, file paths, or key names the plugin uses
The SDK provides convenience functions for constructing credential results:
apiKeyCredential(key, source)— wrap an API keyoauthCredential(accessToken, options)— wrap OAuth tokenscredentialFound(credentials)— success resultcredentialMissing(message?)— no credentials foundcredentialExpired(message?)— token expiredisTokenExpired(expiresAt, bufferMs)— expiry check with buffer
Plugins receive different context objects depending on the method:
| Context | Provided To | Contains |
|---|---|---|
PluginLifecycleContext |
Lifecycle hooks | config, logger |
PluginContext |
auth.discover(), isInstalled() |
config, logger, http, authSources, storage, signal |
ProviderFetchContext |
fetchUsage() |
credentials, http, logger, config, signal, options |
AgentFetchContext |
parseSessions() |
http, logger, config, signal |
NotificationContext |
notify(), initialize() |
logger, config, signal |
The @tokentop/plugin-sdk/testing subpath provides mock factories:
import { createTestContext, createTestProviderFetchContext } from '@tokentop/plugin-sdk/testing';
import { apiKeyCredential } from '@tokentop/plugin-sdk';
// Full plugin context with mocked auth sources
const ctx = createTestContext({
env: { MY_API_KEY: 'test-key' },
files: { '/path/to/auth.json': '{"token": "abc"}' },
httpMocks: {
'https://api.example.com/usage': { status: 200, body: { used: 50 } },
},
});
// Provider-specific fetch context with credentials pre-set
const fetchCtx = createTestProviderFetchContext(
apiKeyCredential('test-key'),
{ httpMocks: { 'https://api.example.com/usage': { status: 200, body: {} } } },
);bun install # Install dependencies
bun run build # Build types + JS
bun run typecheck # TypeScript check (no emit)
bun test # Run tests
bun run clean # Remove dist/Package name: @tokentop/plugin-sdk
Subpath exports:
@tokentop/plugin-sdk— everything (types + helpers)@tokentop/plugin-sdk/types— types only@tokentop/plugin-sdk/helpers— helper utilities only@tokentop/plugin-sdk/testing— test harness and mocks
| This Package (SDK) | Main App (tokentop) |
|---|---|
| Defines interfaces | Implements interfaces |
| Provides type-only context shapes | Creates real sandboxed contexts at runtime |
| Provides mock factories for testing | Provides real HTTP, storage, auth implementations |
| Has zero runtime dependencies | Depends on Bun, SQLite, OpenTUI, etc. |
The main app does NOT depend on this package. It has its own copies of these types (which will be migrated to import from the SDK once the interfaces stabilize). Community plugins depend on this package.
- TypeScript strict mode
- Prefer
constoverlet - Use JSDoc on all public interfaces and methods — this is the primary documentation for plugin developers
- Keep helpers thin — validate + construct, no complex logic
- No runtime dependencies (this is a types + thin helpers package)
- NEVER import from tokentop app internals — this package is standalone
- NEVER add heavy runtime dependencies — keep the package lightweight
- NEVER change required method signatures without bumping
apiVersion - NEVER remove public exports without a major version bump
- Section divider comments (
// ----) are intentional for navigability in type files - JSDoc comments on public types are necessary — they're the plugin developer's primary documentation
- Feature branches from
main - Conventional commits:
feat:,fix:,docs:,refactor: - Types changes that add optional fields:
feat:+ minor version - Types changes that break existing plugins: coordinate with main app release