Skill: notilens/notilens-clawhub-plugin (version 0.2.0)

--- name: notilens description: Send real-time alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking. version: 0.2.0 metadata: openclaw: requires: env: - NOTILENS_TOKEN - NOTILENS_SECRET primaryEnv: NOTILENS_TOKEN emoji: "🔔" homepage: https://www.notilens.com --- # NotiLens Plugin for OpenClaw This is a **code plugin** — all functions are callable directly by the agent at runtime. No curl needed. Get your `NOTILENS_TOKEN` and `NOTILENS_SECRET` from your topic settings at https://www.notilens.com. ## Available Functions ### `notify(name, event, message, options?)` Send a notification. Title is auto-generated from `name + event`. Options: `type`, `image_url`, `open_url`, `download_url`, `tags`, `meta`. ### `track(name, event, message, type?, meta?)` Track any custom event (e.g. `order.placed`, `deploy.started`). Title is auto-generated. ### `taskStarted(name, taskId, message?, meta?)` Fire `task.started` when execution begins. ### `taskProgress(name, taskId, message, meta?)` Fire `task.progress` at meaningful checkpoints. ### `taskCompleted(name, taskId, message, meta?)` Fire `task.completed` when a task finishes successfully. Include `total_duration_ms`, `active_ms`, and custom metrics in `meta`. ### `taskFailed(name, taskId, message, meta?)` Fire `task.failed` when a task fails. Automatically sets `is_actionable: true`. ### `taskError(name, taskId, message, meta?)` Fire `task.error` for non-fatal errors (task continues). ### `taskRetry(name, taskId, retryCount, meta?)` Fire `task.retry` when retrying. Pass the current retry number (1-based). ### `taskLoop(name, taskId, message, loopCount, meta?)` Fire `task.loop` when the same step is repeating. Pass the current loop count. ### `inputRequired(name, message, openUrl?, meta?)` Fire `input.required` when a human decision is needed. Automatically sets `is_actionable: true`. ## Recommended `meta` Fields | Key | Description | |--------------------|-------------| | `run_id` | Unique run ID — format `run_{unix_ms}_{hex4}` | | `total_duration_ms`| Wall-clock time from task start to now | | `active_ms` | Active time (excludes pauses/waits) | | `retry_count` | Number of retries so far | | `error_count` | Number of non-fatal errors | | `loop_count` | Number of loop iterations | | `last_error` | Last error message string | ## Configuration ``` NOTILENS_TOKEN=your_topic_token NOTILENS_SECRET=your_topic_secret ``` Both are found in your topic settings at https://www.notilens.com.

path: claw.json
content: {
  "name": "notilens",
  "version": "0.2.0",
  "description": "Send alerts to NotiLens from any script, app, or AI agent — task lifecycle events, errors, completions, and metric tracking.",
  "author": "notilens",
  "license": "MIT",
  "entry": "src/notilens.js",
  "skills": [
    {
      "id": "genRunId",
      "description": "Generate a unique run ID (run_{unix_ms}_{hex4}) to correlate all events from the same task execution. Call once at task start and include in meta.run_id on every event.",
      "module": "src/notilens.js",
      "export": "genRunId"
    },
    {
      "id": "notify",
      "description": "Send a notification to NotiLens. Pass name (source), event, message, and optional options (type, image_url, open_url, download_url, tags, meta). Title is auto-generated.",
      "module": "src/notilens.js",
      "export": "notify"
    },
    {
      "id": "track",
      "description": "Track any custom event (e.g. order.placed, deploy.started). Title is auto-generated. Type and meta are optional.",
      "module": "src/notilens.js",
      "export": "track"
    },
    {
      "id": "task.queued",
      "description": "Fire task.queued when a task is placed in a queue before a worker picks it up.",
      "module": "src/notilens.js",
      "export": "taskQueued"
    },
    {
      "id": "task.started",
      "description": "Fire task.started when execution begins. Include queue_ms in meta if the task was queued first.",
      "module": "src/notilens.js",
      "export": "taskStarted"
    },
    {
      "id": "task.progress",
      "description": "Fire task.progress at meaningful checkpoints during a long task. Include rows_done, percent, tokens_used, or other metrics in meta.",
      "module": "src/notilens.js",
      "export": "taskProgress"
    },
    {
      "id": "task.paused",
      "description": "Fire task.paused when the task is pausing (e.g. rate limit, waiting on I/O). Include pause_count and wait_reason in meta.",
      "module": "src/notilens.js",
      "export": "taskPaused"
    },
    {
      "id": "task.waiting",
      "description": "Fire task.waiting when the task is blocked on an external resource. Include wait_count and wait_reason in meta.",
      "module": "src/notilens.js",
      "export": "taskWaiting"
    },
    {
      "id": "task.resumed",
      "description": "Fire task.resumed after a pause or wait ends. Include pause_ms or wait_ms in meta.",
      "module": "src/notilens.js",
      "export": "taskResumed"
    },
    {
      "id": "task.retry",
      "description": "Fire task.retry when the task is being retried. Pass the current retry number (1-based) as retryCount.",
      "module": "src/notilens.js",
      "export": "taskRetry"
    },
    {
      "id": "task.loop",
      "description": "Fire task.loop when repeating the same step. Pass the current loop count. Backend ML handles detection.",
      "module": "src/notilens.js",
      "export": "taskLoop"
    },
    {
      "id": "task.error",
      "description": "Fire task.error for a non-fatal error — task continues after this. Include error_count and last_error in meta.",
      "module": "src/notilens.js",
      "export": "taskError"
    },
    {
      "id": "task.completed",
      "description": "Fire task.completed when a task finishes successfully. Include total_duration_ms, active_ms, and custom metrics in meta.",
      "module": "src/notilens.js",
      "export": "taskCompleted"
    },
    {
      "id": "task.failed",
      "description": "Fire task.failed when a task fails and will not be retried. Include retry_count, error_count, last_error, and total_duration_ms in meta.",
      "module": "src/notilens.js",
      "export": "taskFailed"
    },
    {
      "id": "task.timeout",
      "description": "Fire task.timeout when a task exceeds its time limit. Include total_duration_ms and time_limit_ms in meta.",
      "module": "src/notilens.js",
      "export": "taskTimeout"
    },
    {
      "id": "task.cancelled",
      "description": "Fire task.cancelled when a task is cancelled before completion.",
      "module": "src/notilens.js",
      "export": "taskCancelled"
    },
    {
      "id": "task.stopped",
      "description": "Fire task.stopped when a task is stopped intentionally (not an error).",
      "module": "src/notilens.js",
      "export": "taskStopped"
    },
    {
      "id": "task.terminated",
      "description": "Fire task.terminated when a task is forcibly terminated.",
      "module": "src/notilens.js",
      "export": "taskTerminated"
    },
    {
      "id": "input.required",
      "description": "Fire input.required when a human decision is needed to continue. Pass openUrl to link to an approval UI.",
      "module": "src/notilens.js",
      "export": "inputRequired"
    },
    {
      "id": "input.approved",
      "description": "Fire input.approved when a human approves the request.",
      "module": "src/notilens.js",
      "export": "inputApproved"
    },
    {
      "id": "input.rejected",
      "description": "Fire input.rejected when a human rejects the request.",
      "module": "src/notilens.js",
      "export": "inputRejected"
    },
    {
      "id": "output.generated",
      "description": "Fire output.generated when output is produced (file, report, result). Pass download_url, open_url, or image_url in meta.",
      "module": "src/notilens.js",
      "export": "outputGenerated"
    },
    {
      "id": "output.failed",
      "description": "Fire output.failed when expected output could not be produced.",
      "module": "src/notilens.js",
      "export": "outputFailed"
    }
  ],
  "permissions": {
    "network": true,
    "env": [
      "NOTILENS_TOKEN",
      "NOTILENS_SECRET"
    ]
  },
  "engines": {
    "node": ">=18"
  },
  "tags": ["notifications", "monitoring", "alerts", "observability"]
}


path: openclaw.plugin.json
content: {
  "id": "notilens",
  "name": "notilens",
  "description": "Send real-time alerts to NotiLens from any script, app, or AI agent.",
  "entry": "src/notilens.js",
  "exports": {
    "genRunId": "src/notilens.js",
    "notify": "src/notilens.js",
    "track": "src/notilens.js",
    "taskQueued": "src/notilens.js",
    "taskStarted": "src/notilens.js",
    "taskProgress": "src/notilens.js",
    "taskPaused": "src/notilens.js",
    "taskWaiting": "src/notilens.js",
    "taskResumed": "src/notilens.js",
    "taskRetry": "src/notilens.js",
    "taskLoop": "src/notilens.js",
    "taskError": "src/notilens.js",
    "taskCompleted": "src/notilens.js",
    "taskFailed": "src/notilens.js",
    "taskTimeout": "src/notilens.js",
    "taskCancelled": "src/notilens.js",
    "taskStopped": "src/notilens.js",
    "taskTerminated": "src/notilens.js",
    "inputRequired": "src/notilens.js",
    "inputApproved": "src/notilens.js",
    "inputRejected": "src/notilens.js",
    "outputGenerated": "src/notilens.js",
    "outputFailed": "src/notilens.js"
  },
  "configSchema": {
    "type": "object",
    "properties": {
      "token": {
        "type": "string",
        "description": "NotiLens topic token. Found in your topic settings at notilens.com."
      },
      "secret": {
        "type": "string",
        "description": "NotiLens topic secret. Found in your topic settings at notilens.com."
      }
    },
    "required": ["token", "secret"]
  }
}


path: package.json
content: {
  "name": "notilens",
  "version": "0.2.0",
  "description": "NotiLens plugin for OpenClaw — send alerts from any script, app, or AI agent",
  "main": "src/notilens.js",
  "license": "MIT",
  "engines": {
    "node": ">=18"
  },
  "openclaw": {
    "extensions": ["executes-code"],
    "compat": {
      "pluginApi": "1.0"
    },
    "build": {
      "openclawVersion": "1.0.0"
    }
  }
}


path: src/notilens.js
content: 'use strict';

const WEBHOOK_URL = 'https://hook.notilens.com/webhook/{token}/send';
const USER_AGENT = 'notilens-clawhub/0.2.0';

// ── Internals ─────────────────────────────────────────────────────────────────

function getCredentials() {
  const [REDACTED_SECRET];
  const [REDACTED_SECRET];
  if (!token || !secret) {
    throw new Error(
      'NOTILENS_TOKEN and NOTILENS_SECRET environment variables are required. ' +
      'Get them from your topic settings at https://www.notilens.com.'
    );
  }
  return { token, secret };
}

async function _deliver(payload) {
  const { token, secret } = getCredentials();
  const url = WEBHOOK_URL.replace('{token}', token);

  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-NOTILENS-KEY': secret,
      'User-Agent': USER_AGENT,
    },
    body: JSON.stringify({ ts: Date.now() / 1000, ...payload }),
  });

  const data = await res.json().catch(() => ({}));

  if (!res.ok) {
    throw new Error(
      `NotiLens delivery failed: HTTP ${res.status} — ${data.message || data.error || 'unknown error'}`
    );
  }

  return data;
}

function _meta(obj) {
  return Object.keys(obj).length ? { meta: obj } : {};
}

// ── Helper ────────────────────────────────────────────────────────────────────

/**
 * Generate a unique run ID to correlate all events from the same task execution.
 * Format: run_{unix_ms}_{random_hex4}
 * Include this in meta.run_id on every event for a given run.
 */
function genRunId() {
  const hex = Math.floor(Math.random() * 0xffff).toString(16).padStart(4, '0');
  return `run_${Date.now()}_${hex}`;
}

// ── Notify ────────────────────────────────────────────────────────────────────

/**
 * Send a notification. Title is auto-generated from name + event.
 *
 * @param {string} name      - Source name (app, script, agent, etc.)
 * @param {string} event     - Event name, e.g. "order.placed" or "disk.space.full"
 * @param {string} message   - Notification body text
 * @param {object} [options] - type, image_url, open_url, download_url, tags, meta
 */
async function notify(name, event, message, options = {}) {
  if (!event)   throw new Error('event is required');
  if (!message) throw new Error('message is required');
  const { type = 'info', ...rest } = options;
  return _deliver({
    event,
    title: `${name} | ${event}`,
    message,
    type,
    agent: name,  // kept as "agent" for backend compatibility
    ...rest,
  });
}

/**
 * Track any custom event. Title is auto-generated from name + event.
 * Use this for domain-specific events like "order.placed", "deploy.started", etc.
 *
 * @param {string} name
 * @param {string} event     - Any event string, e.g. "order.placed"
 * @param {string} message   - Notification body
 * @param {string} [type]    - "info" | "success" | "warning" | "urgent" (default: "info")
 * @param {object} [meta]    - Optional key-value pairs
 */
async function track(name, event, message, type = 'info', meta = {}) {
  if (!event)   throw new Error('event is required');
  if (!message) throw new Error('message is required');
  return _deliver({
    event,
    title: `${name} | ${event}`,
    message,
    type,
    agent: name,  // kept as "agent" for backend compatibility
    ..._meta(meta),
  });
}

// ── Task lifecycle ─────────────────────────────────────────────────────────────

/**
 * Fire task.queued — task is queued before a worker picks it up.
 */
async function taskQueued(name, taskId, message = '', meta = {}) {
  return _deliver({
    event: 'task.queued',
    title: `${name} | ${taskId} | task.queued`,
    message: message || `${name} | ${taskId} queued`,
    type: 'info',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.started — begins executing a task.
 * @param {object} [meta] - run_id, queue_ms, etc.
 */
async function taskStarted(name, taskId, message = '', meta = {}) {
  return _deliver({
    event: 'task.started',
    title: `${name} | ${taskId} | task.started`,
    message: message || `${name} | ${taskId} started`,
    type: 'info',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.progress — meaningful checkpoint during a long task.
 * @param {object} [meta] - rows_done, percent, tokens_used, etc.
 */
async function taskProgress(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.progress');
  return _deliver({
    event: 'task.progress',
    title: `${name} | ${taskId} | task.progress`,
    message,
    type: 'info',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.paused — task is pausing (rate limit, waiting on I/O, etc.).
 * @param {object} [meta] - pause_count, wait_reason, etc.
 */
async function taskPaused(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.paused');
  return _deliver({
    event: 'task.paused',
    title: `${name} | ${taskId} | task.paused`,
    message,
    type: 'warning',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.waiting — task is blocked on an external resource.
 * @param {object} [meta] - wait_count, wait_reason, etc.
 */
async function taskWaiting(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.waiting');
  return _deliver({
    event: 'task.waiting',
    title: `${name} | ${taskId} | task.waiting`,
    message,
    type: 'warning',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.resumed — task resumed after a pause or wait.
 * @param {object} [meta] - pause_ms, wait_ms, pause_count, wait_count
 */
async function taskResumed(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.resumed');
  return _deliver({
    event: 'task.resumed',
    title: `${name} | ${taskId} | task.resumed`,
    message,
    type: 'info',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.retry — task is being retried after a failure.
 * @param {number} retryCount - Current retry number (1-based)
 * @param {object} [meta]     - last_error, etc.
 */
async function taskRetry(name, taskId, retryCount, meta = {}) {
  return _deliver({
    event: 'task.retry',
    title: `${name} | ${taskId} | task.retry`,
    message: `${name} | ${taskId} retrying (attempt ${retryCount})`,
    type: 'warning',
    agent: name,
    task_id: taskId,
    meta: { retry_count: retryCount, ...meta },
  });
}

/**
 * Fire task.loop — agent detected it is repeating the same step.
 * @param {number} loopCount - How many times the step has repeated
 * @param {object} [meta]
 */
async function taskLoop(name, taskId, message, loopCount, meta = {}) {
  if (!message) throw new Error('message is required for task.loop');
  return _deliver({
    event: 'task.loop',
    title: `${name} | ${taskId} | task.loop`,
    message,
    type: 'warning',
    agent: name,
    task_id: taskId,
    is_actionable: true,
    meta: { loop_count: loopCount, ...meta },
  });
}

/**
 * Fire task.error — non-fatal error (task continues after this).
 * @param {object} [meta] - error_count, last_error, etc.
 */
async function taskError(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.error');
  return _deliver({
    event: 'task.error',
    title: `${name} | ${taskId} | task.error`,
    message,
    type: 'urgent',
    agent: name,
    task_id: taskId,
    is_actionable: true,
    ..._meta(meta),
  });
}

// ── Terminal states ────────────────────────────────────────────────────────────

/**
 * Fire task.completed — task finished successfully.
 * @param {object} [meta]               - total_duration_ms, active_ms, rows_processed, etc.
 * @param {string} [meta.download_url]  - Promoted to top-level field
 * @param {string} [meta.open_url]      - Promoted to top-level field
 */
async function taskCompleted(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.completed');
  const { download_url, open_url, ...restMeta } = meta;
  return _deliver({
    event: 'task.completed',
    title: `${name} | ${taskId} | task.completed`,
    message,
    type: 'success',
    agent: name,
    task_id: taskId,
    ...(download_url ? { download_url } : {}),
    ...(open_url     ? { open_url }     : {}),
    ..._meta(restMeta),
  });
}

/**
 * Fire task.failed — task failed and will not be retried.
 * @param {object} [meta]          - retry_count, error_count, last_error, total_duration_ms
 * @param {string} [meta.open_url] - Promoted to top-level field
 */
async function taskFailed(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.failed');
  const { open_url, ...restMeta } = meta;
  return _deliver({
    event: 'task.failed',
    title: `${name} | ${taskId} | task.failed`,
    message,
    type: 'urgent',
    agent: name,
    task_id: taskId,
    is_actionable: true,
    ...(open_url ? { open_url } : {}),
    ..._meta(restMeta),
  });
}

/**
 * Fire task.timeout — task exceeded its time limit.
 * @param {object} [meta] - total_duration_ms, time_limit_ms, etc.
 */
async function taskTimeout(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.timeout');
  return _deliver({
    event: 'task.timeout',
    title: `${name} | ${taskId} | task.timeout`,
    message,
    type: 'urgent',
    agent: name,
    task_id: taskId,
    is_actionable: true,
    ..._meta(meta),
  });
}

/**
 * Fire task.cancelled — task was cancelled before completion.
 * @param {object} [meta] - total_duration_ms, etc.
 */
async function taskCancelled(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.cancelled');
  return _deliver({
    event: 'task.cancelled',
    title: `${name} | ${taskId} | task.cancelled`,
    message,
    type: 'warning',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.stopped — task was stopped intentionally (not an error).
 * @param {object} [meta] - total_duration_ms, etc.
 */
async function taskStopped(name, taskId, message = '', meta = {}) {
  return _deliver({
    event: 'task.stopped',
    title: `${name} | ${taskId} | task.stopped`,
    message: message || `${name} | ${taskId} stopped`,
    type: 'info',
    agent: name,
    task_id: taskId,
    ..._meta(meta),
  });
}

/**
 * Fire task.terminated — task was forcibly terminated.
 * @param {object} [meta] - total_duration_ms, reason, etc.
 */
async function taskTerminated(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for task.terminated');
  return _deliver({
    event: 'task.terminated',
    title: `${name} | ${taskId} | task.terminated`,
    message,
    type: 'urgent',
    agent: name,
    task_id: taskId,
    is_actionable: true,
    ..._meta(meta),
  });
}

// ── Input ──────────────────────────────────────────────────────────────────────

/**
 * Fire input.required — needs a human decision to continue.
 * @param {string} [openUrl] - URL to open for the approval UI
 * @param {object} [meta]
 */
async function inputRequired(name, message, openUrl = '', meta = {}) {
  if (!message) throw new Error('message is required for input.required');
  return _deliver({
    event: 'input.required',
    title: `${name} | input required`,
    message,
    type: 'warning',
    agent: name,
    is_actionable: true,
    ...(openUrl ? { open_url: openUrl } : {}),
    ..._meta(meta),
  });
}

/**
 * Fire input.approved — human approved the request.
 */
async function inputApproved(name, message, meta = {}) {
  if (!message) throw new Error('message is required for input.approved');
  return _deliver({
    event: 'input.approved',
    title: `${name} | input approved`,
    message,
    type: 'success',
    agent: name,
    ..._meta(meta),
  });
}

/**
 * Fire input.rejected — human rejected the request.
 */
async function inputRejected(name, message, meta = {}) {
  if (!message) throw new Error('message is required for input.rejected');
  return _deliver({
    event: 'input.rejected',
    title: `${name} | input rejected`,
    message,
    type: 'warning',
    agent: name,
    is_actionable: true,
    ..._meta(meta),
  });
}

// ── Output ─────────────────────────────────────────────────────────────────────

/**
 * Fire output.generated — produced output (file, report, result, etc.).
 * @param {string} [meta.download_url] - Promoted to top-level field
 * @param {string} [meta.open_url]     - Promoted to top-level field
 * @param {string} [meta.image_url]    - Promoted to top-level field
 */
async function outputGenerated(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for output.generated');
  const { download_url, open_url, image_url, ...restMeta } = meta;
  return _deliver({
    event: 'output.generated',
    title: `${name} | ${taskId} | output.generated`,
    message,
    type: 'success',
    agent: name,
    task_id: taskId,
    ...(download_url ? { download_url } : {}),
    ...(open_url     ? { open_url }     : {}),
    ...(image_url    ? { image_url }    : {}),
    ..._meta(restMeta),
  });
}

/**
 * Fire output.failed — failed to produce expected output.
 * @param {object} [meta] - last_error, etc.
 */
async function outputFailed(name, taskId, message, meta = {}) {
  if (!message) throw new Error('message is required for output.failed');
  return _deliver({
    event: 'output.failed',
    title: `${name} | ${taskId} | output.failed`,
    message,
    type: 'urgent',
    agent: name,
    task_id: taskId,
    is_actionable: true,
    ..._meta(meta),
  });
}

// ── Exports ────────────────────────────────────────────────────────────────────

module.exports = {
  genRunId,
  notify,
  track,
  taskQueued,
  taskStarted,
  taskProgress,
  taskPaused,
  taskWaiting,
  taskResumed,
  taskRetry,
  taskLoop,
  taskError,
  taskCompleted,
  taskFailed,
  taskTimeout,
  taskCancelled,
  taskStopped,
  taskTerminated,
  inputRequired,
  inputApproved,
  inputRejected,
  outputGenerated,
  outputFailed,
};


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)