Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions plugins/notifier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# notifier

Sends a cross-platform OS notification when a Cline run completes.

## What It Does

Registers an `afterRun` hook that sends a native desktop notification on run completion. Uses a strategy pattern with notification systems:

- `NotificationCenter` -- macOS native alerts
- `NotifySend` -- Linux desktop notifications (libnotify)
- `WindowsToaster` -- Windows toast notifications (Windows 8+)

The appropriate strategy is automatically selected based on the current platform.

## Install

```bash
cline plugin install notifier
```

For local development from this repository:

```bash
cline plugin install ./plugins/notifier --cwd .
```

## Example Usage

After installation, ask Cline:

```text
Run the test suite and let me know when the task is complete.
```

When the Cline run completes, `notifier` sends a native desktop notification with a short completion summary.

## Requirements

- macOS, Linux, or Windows.
- Linux requires `libnotify-bin` or equivalent (`notify-send` command).
- No API keys or external services.

## Security Notes

Notification text can include task output. Avoid enabling it if completion summaries may contain sensitive information.
176 changes: 176 additions & 0 deletions plugins/notifier/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Notifier Plugin
*
* Cross-platform OS notifications when a Cline run completes.
* Uses a strategy pattern where notification strategies are named after
* their respective OS notification systems:
* - NotificationCenter (macOS)
* - NotifySend (Linux)
* - WindowsToaster (Windows)
*
* CLI usage:
* cline plugin install notifier
* cline -i "Run the test suite"
*/

import type { AgentPlugin, AgentRunResult } from "@cline/core";

// ---------------------------------------------------------------------------
// NotifierStrategy interface -- strategy names correspond to OS notification
// systems supported by the node-notifier library.
// ---------------------------------------------------------------------------

interface NotifierStrategy {
/** Human-readable name of the notification system (e.g. "NotificationCenter"). */
readonly systemName: string;

/** Whether this notifier is supported on the current platform. */
isSupported(): boolean;

/** Send a notification. No-op if unsupported. */
notify(title: string, message: string): void;
}

// ---------------------------------------------------------------------------
// Concrete strategies -- one per supported OS notification system.
// ---------------------------------------------------------------------------

/** macOS Notification Center (native alerts on darwin). */
class NotificationCenterNotifier implements NotifierStrategy {
readonly systemName = "NotificationCenter";

isSupported(): boolean {
return process.platform === "darwin";
}

notify(title: string, message: string): void {
if (!this.isSupported()) {
return;
}
// Use dynamic import so node-notifier is only required at runtime on macOS.
import("node-notifier")
.then((notifier) => {
notifier.notify({ title, message, sound: true }, (err: Error | null) => {
if (err) {
console.error(`[notifier] NotificationCenter error: ${err.message}`);
}
});
})
.catch((err: unknown) => {
console.error(`[notifier] Failed to load node-notifier: ${String(err)}`);
});
}
}

/** Linux desktop notifications via notify-send / libnotify. */
class NotifySendNotifier implements NotifierStrategy {
readonly systemName = "NotifySend";

isSupported(): boolean {
return process.platform === "linux";
}

notify(title: string, message: string): void {
if (!this.isSupported()) {
return;
}
import("node-notifier")
.then((notifier) => {
const n =
typeof notifier.NotificationCenter === "function"
? new notifier.NotificationCenter()
: notifier;
n.notify({ title, message, sound: true }, (err: Error | null) => {
if (err) {
console.error(`[notifier] NotifySend error: ${err.message}`);
}
});
})
.catch((err: unknown) => {
console.error(`[notifier] Failed to load node-notifier: ${String(err)}`);
});
}
}

/** Windows toast notifications (Windows 8+ / Windows 10 / Windows 11). */
class WindowsToasterNotifier implements NotifierStrategy {
readonly systemName = "WindowsToaster";

isSupported(): boolean {
return process.platform === "win32";
}

notify(title: string, message: string): void {
if (!this.isSupported()) {
return;
}
import("node-notifier")
.then((notifier) => {
const n =
typeof notifier.WindowsToaster === "function"
? new notifier.WindowsToaster()
: notifier;
n.notify({ title, message, sound: true }, (err: Error | null) => {
if (err) {
console.error(`[notifier] WindowsToaster error: ${err.message}`);
}
});
})
.catch((err: unknown) => {
console.error(`[notifier] Failed to load node-notifier: ${String(err)}`);
});
}
}

// ---------------------------------------------------------------------------
// Strategy resolver -- picks the right notifier for the current platform.
// ---------------------------------------------------------------------------

const strategies: NotifierStrategy[] = [
new NotificationCenterNotifier(),
new NotifySendNotifier(),
new WindowsToasterNotifier(),
];

function resolveNotifier(): NotifierStrategy | undefined {
return strategies.find((s) => s.isSupported());
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function summarizeResult(result: AgentRunResult): string {
const summary = result.outputText.trim();
if (summary.length > 0) {
return summary;
}
return `Completed in ${result.iterations} iteration(s).`;
}

// ---------------------------------------------------------------------------
// Plugin definition
// ---------------------------------------------------------------------------

const plugin: AgentPlugin = {
name: "notifier-on-complete",
manifest: {
capabilities: ["hooks"],
},

hooks: {
afterRun({ result }) {
if (result.status !== "completed") {
return;
}
const notifier = resolveNotifier();
if (!notifier) {
return;
}
notifier.notify("Cline session completed", summarizeResult(result));
},
},
};

export { plugin };
export default plugin;
18 changes: 18 additions & 0 deletions plugins/notifier/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "notifier",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cross-platform OS notifications when a Cline run completes, using a strategy pattern for different notification systems.",
"dependencies": {
"node-notifier": "^10.0.1"
},
"cline": {
"plugins": [
{
"paths": ["./index.ts"],
"capabilities": ["hooks"]
}
]
}
}