Skill: whatsable/whatsapp-business-automation-by-whatsable (version 1.0.2)

--- name: whatsapp-business-agent-skills description: > Full three-phase agent skills suite for Notifyer by WhatsAble. Phase 1 (setup-notifyer): account signup, login, WhatsApp connection status, subscription plans, team members, workspace labels, and Developer API key. Phase 2 (automate-notifyer): WhatsApp message templates, AI bots, bulk broadcast campaigns, messaging analytics, delivery logs, and developer/IO webhooks. Phase 3 (chat-notifyer): live WhatsApp chat operations including listing and searching conversations, sending text/template/attachment messages, managing labels, controlling AI bot vs human handoff, scheduling messages, and managing recipient notes. All three phases are bundled together; each phase depends on the previous one. license: Proprietary — © WhatsAble. All rights reserved. compatibility: Requires Node.js >= 18. Set NOTIFYER_API_BASE_URL and NOTIFYER_API_TOKEN before running any script. No npm dependencies — uses Node.js built-ins only. metadata: {"author":"whatsable","version":"0.4.0","product":"Notifyer by WhatsAble","api-base":"https://api.insightssystem.com","homepage":"https://github.com/Whatsable/whatsapp-business-agent-skills","env":"NOTIFYER_API_BASE_URL, NOTIFYER_API_TOKEN"} --- # Notifyer Agent Skills Agent skills for [Notifyer by WhatsAble](https://notifyer-systems.com) — three phases that together cover the full platform: account setup, automation infrastructure, and live chat operations. Scripts are self-contained Node.js 18+ files with no external npm dependencies. Source repository: <https://github.com/Whatsable/whatsapp-business-agent-skills> --- ## Phase overview | Phase | Skill folder | Coverage | |-------|-------------|----------| | 1 | `skills/setup-notifyer` | Account, login, WhatsApp connection, plans, team, labels, API key | | 2 | `skills/automate-notifyer` | Templates, AI bots, broadcasts, analytics, webhooks | | 3 | `skills/chat-notifyer` | Recipients, messaging, labels, handoff, scheduling, notes | Each phase depends on the one before it. Obtain `NOTIFYER_API_TOKEN` from Phase 1 (`skills/setup-notifyer/scripts/login.js`) before using Phase 2 or Phase 3. --- ## Environment variables | Variable | Required | Description | |----------|----------|-------------| | `NOTIFYER_API_BASE_URL` | **yes** | API base URL — use `https://api.insightssystem.com` | | `NOTIFYER_API_TOKEN` | **yes** | JWT token from `setup-notifyer/scripts/login.js` | | `NOTIFYER_CHAT_ORIGIN` | no | CORS Origin override for Phase 3 chat endpoints (default: `https://chat.notifyer-systems.com`) | ```bash export NOTIFYER_API_BASE_URL="https://api.insightssystem.com" export NOTIFYER_API_TOKEN="<jwt-from-login.js>" ``` `NOTIFYER_API_BASE_URL` must start with `https://` — all three skill sets enforce this at startup and exit immediately on a non-HTTPS value to prevent token exposure. --- ## Authentication modes | Mode | Header format | Used by | |------|--------------|---------| | Console | `Authorization: Bearer <token>` | Phase 1, Phase 2, and most Phase 3 scripts | | Chat | `Authorization: <token>` (no Bearer prefix) | Phase 3 chat endpoints, Phase 1 label endpoints | | Developer | `Authorization: <api_key>` (no Bearer prefix) | External tools (Make / Zapier / n8n) via `get-api-key.js` | The same JWT token (`NOTIFYER_API_TOKEN`) works for both Console and Chat modes. `notifyer-api.js` selects the correct header format automatically per endpoint. --- ## Phase 1 — setup-notifyer Scripts live in `skills/setup-notifyer/scripts/`. **Start here.** Login to get `NOTIFYER_API_TOKEN`, then verify the workspace is ready with `doctor.js` before running Phase 2 or Phase 3 scripts. ### Key commands ```bash # Create a new account node skills/setup-notifyer/scripts/create-account.js \ --name "Jane Smith" --email [REDACTED_SECRET] \ --password "Secure@123" --phone 14155550123 # Login and export the token node skills/setup-notifyer/scripts/login.js \ --email [REDACTED_SECRET] --password "Secure@123" export NOTIFYER_API_TOKEN="<authToken from above>" # Pre-flight health check (validates URL, token, WhatsApp connection, plan) node skills/setup-notifyer/scripts/doctor.js --pretty # Check WhatsApp connection status node skills/setup-notifyer/scripts/get-connection-status.js --pretty # List team members node skills/setup-notifyer/scripts/list-members.js --pretty # List workspace labels node skills/setup-notifyer/scripts/list-labels.js --pretty # Retrieve Developer API key (for Make / Zapier / n8n) node skills/setup-notifyer/scripts/get-api-key.js --pretty ``` For the full Phase 1 script reference see `skills/setup-notifyer/SKILL.md`. --- ## Phase 2 — automate-notifyer Scripts live in [REDACTED_SECRET]. Requires: completed Phase 1 with `NOTIFYER_API_TOKEN` set, WhatsApp number connected, and (for bots and broadcasts) a Pro or Agency subscription. ### Key commands ```bash # Templates node skills/automate-notifyer/scripts/list-templates.js --status approved --pretty node skills/automate-notifyer/scripts/create-template.js \ --name order_confirmation --category MARKETING \ --body "Hello {{1}}, your order #{{2}} is confirmed." \ --variables '{"1":"John","2":"12345"}' # AI bots node skills/automate-notifyer/scripts/list-bots.js --pretty node skills/automate-notifyer/scripts/create-bot.js \ --name "Support Bot" --mission "Help resolve support issues." \ --tone "Friendly" --delay 3 --default # Broadcasts node skills/automate-notifyer/scripts/list-broadcasts.js --status upcoming --pretty node skills/automate-notifyer/scripts/create-broadcast.js \ --name "January Sale" --template-id 42 \ --test-phone "+14155550123" \ --recipients ./recipients.csv \ --schedule "25/01/2025 14:00" \ --delivery-mode smart # Analytics node skills/automate-notifyer/scripts/get-message-analytics.js --days 30 --pretty # Webhooks node skills/automate-notifyer/scripts/list-webhooks.js --type dev --pretty node skills/automate-notifyer/scripts/create-webhook.js \ --url "https://hook.eu2.make.com/abc" --incoming --outgoing --signature ``` For the full Phase 2 script reference see `skills/automate-notifyer/SKILL.md`. --- ## Phase 3 — chat-notifyer Scripts live in `skills/chat-notifyer/scripts/`. Requires: completed Phase 1 with `NOTIFYER_API_TOKEN` set and WhatsApp number connected. Phase 3 endpoints use **Chat auth mode**: `Authorization: <token>` (no Bearer prefix). The same JWT from `login.js` works — `notifyer-api.js` switches the header format automatically. ### WhatsApp 24-hour messaging window Free-text messages and attachments can only be sent within 24 hours of the recipient's last inbound message. Check `recipient.expiration_timestamp` to determine window state: - Window open (`expiration_timestamp > Date.now()`): text, template, and attachment sends are allowed - Window closed (`null` or past): template-only sends are allowed ### Key commands ```bash # List all active conversations node skills/chat-notifyer/scripts/list-recipients.js --pretty # Search for a contact node skills/chat-notifyer/scripts/list-recipients.js --search "John" --pretty # Get full recipient details (includes 24h window state) node skills/chat-notifyer/scripts/get-recipient.js --phone 14155550123 --pretty # Send a text message node skills/chat-notifyer/scripts/send-text.js \ --phone 14155550123 --text "Hello! How can I help?" # Send a template message node skills/chat-notifyer/scripts/send-template.js \ --phone 14155550123 --template order_confirmation \ --variables '{"1":"John","2":"12345"}' # Send an attachment node skills/chat-notifyer/scripts/send-attachment.js \ --phone 14155550123 --file ./invoice.pdf --caption "Your invoice" # Assign a label node skills/chat-notifyer/scripts/assign-label.js \ --phone 14155550123 --labels "Support,VIP" # Control AI bot vs human handoff node skills/chat-notifyer/scripts/set-handoff.js \ --phone 14155550123 --handoff true # true = human handles; false = bot handles # Schedule a message node skills/chat-notifyer/scripts/send-template.js \ --phone 14155550123 --template order_confirmation \ --variables '{"1":"John","2":"12345"}' \ --schedule "2025-06-01T09:00:00" # Add a note to a conversation node skills/chat-notifyer/scripts/add-note.js \ --phone 14155550123 --note "VIP customer — apply 15% discount" # Get conversation history node skills/chat-notifyer/scripts/get-conversation.js \ --phone 14155550123 --pretty ``` For the full Phase 3 script reference see `skills/chat-notifyer/SKILL.md`. --- ## Cross-phase usage notes - Run `skills/setup-notifyer/scripts/doctor.js --pretty` as a first step when troubleshooting any script failure. It validates base URL, token, WhatsApp connection, and plan status in one pass. - Phone numbers are always integers without the `+` prefix (e.g. `14155550123`). Scripts strip the `+` automatically when it is supplied. - `NOTIFYER_API_BASE_URL` is validated at startup in every script. An `http://` value is rejected immediately — HTTPS is required to prevent token exposure. - AI Bots and Broadcasts (Phase 2) require a Pro or Agency subscription. Verify plan status with `get-user-plan.js` before directing users to those features. - The WhatsApp initial connection (WABA embedded signup) cannot be scripted. A workspace admin must complete it once via the Notifyer console browser UI at `console.notifyer-systems.com`. After that, all connection management is scriptable. - Subscription and billing changes are browser-only Stripe flows. Direct users to `https://console.notifyer-systems.com/pricing-plans` for plan changes. --- ## Security - **Zero npm dependencies.** All scripts use only Node.js built-in modules — no third-party packages, no supply chain risk. - **Token handling.** `login.js` prints the JWT to stdout by design so the agent can capture and export it. Treat it like a session cookie and avoid persisting it in unprotected logs. - **Developer API key.** `get-api-key.js` outputs the key to stdout and stderr. Store it in a secrets manager immediately. It is a long-lived credential that grants direct WhatsApp send access. - **HTTPS enforcement.** All scripts call `loadConfig()` which exits with an error if `NOTIFYER_API_BASE_URL` does not start with `https://`. --- ## Repository structure ``` skills/ setup-notifyer/ Phase 1 — account, auth, connection, team, labels SKILL.md scripts/ references/ assets/ automate-notifyer/ Phase 2 — templates, bots, broadcasts, analytics, webhooks SKILL.md scripts/ references/ assets/ chat-notifyer/ Phase 3 — recipients, messaging, handoff, scheduling, notes SKILL.md scripts/ references/ assets/ ``` Each phase is also published separately on ClawHub as an individual skill (`setup-notifyer`, `automate-notifyer`, `chat-notifyer`) under the `@whatsable` namespace.

path: PaxHeader/clawhub.json
content: 29 mtime=1776964078.72844559
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: clawhub.json
content: {
  "name": "whatsapp-business-agent-skills",
  "tagline": "Full three-phase agent skill suite for Notifyer by WhatsAble",
  "description": "Complete agent skills suite for Notifyer by WhatsAble covering all three phases of the platform. Phase 1 (setup-notifyer): account signup, login, WhatsApp connection, subscription plans, team members, workspace labels, and Developer API key. Phase 2 (automate-notifyer): WhatsApp message templates, AI bots, bulk broadcast campaigns, messaging analytics, delivery logs, and developer/IO webhooks for Make, Zapier, and n8n. Phase 3 (chat-notifyer): live WhatsApp chat operations including listing conversations, sending text/template/attachment messages, assigning labels, controlling AI bot vs human handoff, scheduling messages, and managing notes. All three phases are bundled together and required for full platform coverage.",
  "category": "productivity",
  "tags": [
    "whatsapp",
    "notifyer",
    "whatsable",
    "messaging",
    "automation",
    "chat",
    "live-chat",
    "broadcasts",
    "templates",
    "webhooks",
    "bots",
    "analytics",
    "setup",
    "authentication",
    "scheduling",
    "handoff"
  ],
  "version": "0.4.0",
  "author": "whatsable",
  "license": "Proprietary",
  "homepage": "https://github.com/Whatsable/whatsapp-business-agent-skills",
  "repository": "https://github.com/Whatsable/whatsapp-business-agent-skills",
  "requires": {
    "node": ">=18"
  },
  "env": [
    {
      "name": "NOTIFYER_API_BASE_URL",
      "description": "Base URL for the Notifyer API. Use https://api.insightssystem.com. Must start with https:// — all scripts enforce this at startup.",
      "required": true
    },
    {
      "name": "NOTIFYER_API_TOKEN",
      "description": "JWT token obtained from skills/setup-notifyer/scripts/login.js. Required for all API calls across all three phases.",
      "required": true
    },
    {
      "name": "NOTIFYER_CHAT_ORIGIN",
      "description": "Optional CORS Origin override for Phase 3 chat endpoints. Defaults to https://chat.notifyer-systems.com. Only needed if you require a different origin header.",
      "required": false
    }
  ]
}


path: PaxHeader/update-skill-filemap.mjs
content: 30 mtime=1776964078.728956464
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: update-skill-filemap.mjs
content: #!/usr/bin/env node

import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const MARKER_BEGIN = '<!-- FILEMAP:BEGIN -->'
const MARKER_END = '<!-- FILEMAP:END -->'

const IGNORED_DIRS = new Set(['node_modules', '.git', '.DS_Store'])
const IGNORED_FILES = new Set(['.DS_Store'])

function listSkillDirs(skillsRoot) {
  const entries = readdirSync(skillsRoot, { withFileTypes: true })
  return entries
    .filter((e) => e.isDirectory())
    .map((e) => path.join(skillsRoot, e.name))
    .filter((dir) => {
      try {
        return statSync(path.join(dir, 'SKILL.md')).isFile()
      } catch {
        return false
      }
    })
    .sort()
}

function walkDir(skillRoot, relDir = '.', out = new Map()) {
  const absDir = relDir === '.' ? skillRoot : path.join(skillRoot, relDir)
  let entries
  try {
    entries = readdirSync(absDir, { withFileTypes: true })
  } catch {
    return out
  }

  const files = []
  const dirs = []

  for (const entry of entries) {
    if (entry.name.startsWith('.') && entry.name !== '.well-known') continue
    if (entry.isDirectory()) {
      if (IGNORED_DIRS.has(entry.name)) continue
      dirs.push(entry.name)
      continue
    }
    if (entry.isFile()) {
      if (IGNORED_FILES.has(entry.name)) continue
      files.push(entry.name)
      continue
    }
  }

  files.sort((a, b) => a.localeCompare(b))
  dirs.sort((a, b) => a.localeCompare(b))

  if (files.length > 0) {
    out.set(relDir, files)
  }

  for (const d of dirs) {
    const nextRel = relDir === '.' ? d : path.join(relDir, d)
    walkDir(skillRoot, nextRel, out)
  }

  return out
}

function formatFileMap({ skillName, map }) {
  const lines = [`[${skillName} file map]|root: .`]
  const keys = [...map.keys()].sort((a, b) => a.localeCompare(b))

  for (const dir of keys) {
    const files = map.get(dir) || []
    const label = dir === '.' ? '.' : dir.replaceAll('\\', '/')
    lines.push(`|${label}:{${files.join(',')}}`)
  }

  return lines.join('\n')
}

function upsertFileMapSection(skillMd, fileMapText) {
  const block = `${MARKER_BEGIN}\n\`\`\`text\n${fileMapText}\n\`\`\`\n${MARKER_END}\n`

  const beginIndex = skillMd.indexOf(MARKER_BEGIN)
  const endIndex = skillMd.indexOf(MARKER_END)

  if (beginIndex >= 0 && endIndex >= 0 && endIndex > beginIndex) {
    const before = skillMd.slice(0, beginIndex).replace(/\s*$/, '\n\n')
    const after = skillMd.slice(endIndex + MARKER_END.length).replace(/^\s*/, '\n')
    return `${before}${block}${after}`.replace(/\n{4,}/g, '\n\n\n')
  }

  return `${skillMd.replace(/\s*$/, '\n\n')}${block}`
}

function main() {
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
  const skillsRoot = path.join(scriptDir, 'skills')

  const skillDirs = listSkillDirs(skillsRoot)
  if (skillDirs.length === 0) {
    console.error(`No skills found under: ${skillsRoot}`)
    process.exit(1)
  }

  for (const dir of skillDirs) {
    const skillMdPath = path.join(dir, 'SKILL.md')
    const skillName = path.basename(dir)

    const original = readFileSync(skillMdPath, 'utf8')
    const map = walkDir(dir)
    const fileMapText = formatFileMap({ skillName, map })
    const updated = upsertFileMapSection(original, fileMapText)

    if (updated !== original) {
      writeFileSync(skillMdPath, updated, 'utf8')
      console.log(`Updated: ${path.relative(scriptDir, skillMdPath)}`)
    } else {
      console.log(`Unchanged: ${path.relative(scriptDir, skillMdPath)}`)
    }
  }
}

main()



path: PaxHeader/README.md
content: 30 mtime=1776964078.729798755
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: README.md
content: # Notifyer Agent Skills

<img src="https://res.cloudinary.com/subframe/image/upload/v1756457825/uploads/4086/b4ynd9jid16pcby8cfz0.svg" alt="Notifyer by WhatsAble" height="40" />

> **v0.4.0 — Full suite ClawHub release.** `setup-notifyer`, `automate-notifyer`, and `chat-notifyer` are all production-ready.

Per-skill versions (kept in sync with each skill’s `package.json`, `SKILL.md` frontmatter, and `clawhub.json`): **setup-notifyer** 1.0.0; **automate-notifyer** and **chat-notifyer** 0.3.0.

Agent Skills for [Notifyer by WhatsAble](https://notifyer-systems.com) — built on the open [AgentSkills](https://agentskills.io) format. These skills teach AI coding agents how to authenticate, configure, and operate a Notifyer workspace programmatically, using the same API surface as the Notifyer Console and Chat applications.

Works across any compatible agent — **OpenClaw, Cursor, Claude Code, GitHub Copilot, Gemini CLI, Amp, Roo Code, Junie, OpenHands**, and [many more](https://agentskills.io).

---

## Table of Contents

- [What is this?](#what-is-this)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [Authentication Modes](#authentication-modes)
- [Available Skills](#available-skills)
- [Script Reference — setup-notifyer](#script-reference--setup-notifyer)
- [Script Reference — automate-notifyer](#script-reference--automate-notifyer)
- [Script Reference — chat-notifyer](#script-reference--chat-notifyer)
- [Use Cases — What AI Agents Can Do](#use-cases--what-ai-agents-can-do)
- [Limitations](#limitations)
- [Repository Structure](#repository-structure)
- [AgentSkills Format](#agentskills-format)
- [Learn More](#learn-more)

---

## What is this?

Notifyer is a WhatsApp Business automation platform by WhatsAble. It lets businesses connect their WhatsApp Business number, build message templates, manage team inboxes with labels and roles, run AI chatbots, send automated broadcasts, and integrate with Make, Zapier, n8n, or a direct developer API.

This repository packages Notifyer's full management and chat capabilities as **Agent Skills** — a standardised, agent-readable format that any compatible AI coding agent can discover and use. Agents that load these skills can set up, configure, automate, and operate Notifyer workspaces as part of larger automated workflows, without ever opening the browser console.

---

## Requirements

- **Node.js >= 18** (uses native `fetch` and ESM imports — no dependencies)
- A Notifyer account at [console.notifyer-systems.com](https://console.notifyer-systems.com)
- For API key usage (Make/Zapier/n8n integrations): a **Pro or Agency** subscription

---

## Installation

### With a compatible agent (recommended)

If your agent supports the AgentSkills format, point it to this repository:

```bash
# Interactive — choose which skills to install:
npx skills add whatsable/whatsapp-business-agent-skills

# Install all 3 skills at once (no prompt):
npx skills add whatsable/whatsapp-business-agent-skills --all

# Install a specific skill only:
npx skills add whatsable/whatsapp-business-agent-skills --skill chat-notifyer
```

The agent will discover all available skills and load them on demand.

### Manual clone

```bash
git clone https://github.com/Whatsable/whatsapp-business-agent-skills
cd whatsapp-business-agent-skills
```

Each skill is self-contained — no external dependencies. Run scripts directly with Node.js 18+.

---

## Quick Start

**Step 1 — Set the API base URL:**

```bash
export NOTIFYER_API_BASE_URL="https://api.insightssystem.com"
```

**Step 2 — Log in and capture your token:**

```bash
cd skills/setup-notifyer
node scripts/login.js --email [REDACTED_SECRET] --password "YourPassword@1"
# Output: { "ok": true, "data": { "authToken": "eyJ..." } }
export NOTIFYER_API_TOKEN="eyJ..."
```

**Step 3 — Verify identity and connection:**

```bash
node scripts/get-me.js --pretty
node scripts/get-connection-status.js --pretty
```

You are now ready to run scripts from any of the three skills. The same `NOTIFYER_API_TOKEN` works across all skills.

---

## Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `NOTIFYER_API_BASE_URL` | **yes** | — | API host — always `https://api.insightssystem.com` |
| `NOTIFYER_API_TOKEN` | **yes** (most scripts) | — | JWT from `login.js`. Not needed for `create-account.js` or `login.js` themselves. |
| `NOTIFYER_CHAT_ORIGIN` | no | `https://chat.notifyer-systems.com` | CORS `Origin` header for chat endpoints. Only needed if your Notifyer instance uses a different domain. |

All variables are loaded by `scripts/lib/notifyer-api.js`. If a required variable is missing, the script exits immediately with a clear error.

### Persisting across sessions

```bash
# Add to ~/.zshrc or ~/.bashrc
export NOTIFYER_API_BASE_URL="https://api.insightssystem.com"
export NOTIFYER_API_TOKEN="eyJ..."
```

Tokens are JWTs with an expiry. If a script returns HTTP 401, re-run `login.js` to refresh.

---

## Authentication Modes

Notifyer's backend uses **three distinct auth modes** depending on which API surface is called. All scripts handle this automatically — you only ever set `NOTIFYER_API_TOKEN` once.

| Mode | Header Format | Used By |
|------|--------------|---------|
| **Console** | `Authorization: Bearer <jwt>` | `setup-notifyer` + most of `automate-notifyer` |
| **Chat** | `Authorization: <jwt>` (raw, no prefix) | `chat-notifyer` + label scripts |
| **Developer** | `Authorization: <api_key>` (raw) | External tools: Make, Zapier, n8n |

The **same JWT** from `login.js` works for both Console and Chat modes — only the header format differs. Scripts automatically select the correct mode. The Developer API key is a separate credential retrieved with `get-api-key.js`.

---

## Available Skills

| Skill | Status | Phase | Description |
|-------|--------|-------|-------------|
| [`setup-notifyer`](skills/setup-notifyer/) | **Production-ready** | 1 | Account, WhatsApp connection, plans, team & roles, labels, Developer API key |
| [`automate-notifyer`](skills/automate-notifyer/) | **Production-ready** | 2 | Templates, AI bots, broadcasts, analytics, webhooks |
| [`chat-notifyer`](skills/chat-notifyer/) | **Production-ready** | 3 | Recipients, messaging, labels, AI handoff, scheduled messages, notes |

All three skills use the same `NOTIFYER_API_TOKEN`. Run scripts from within each skill's directory.

---

## Script Reference — setup-notifyer

```bash
cd skills/setup-notifyer
node scripts/<script>.js [flags]
```

### Pre-flight Health Check

| Script | Description |
|--------|-------------|
| `doctor.js` | Validate base URL, token, WhatsApp connection, and plan in one command |

```bash
node scripts/doctor.js --pretty   # run this first when debugging unexpected failures
```

### Account

| Script | Description |
|--------|-------------|
| `create-account.js` | Create a new Notifyer workspace |
| `login.js` | Login and get a JWT auth token |
| `get-me.js` | Get the authenticated user's profile |

```bash
node scripts/create-account.js --name "Jane" --email [REDACTED_SECRET] --password "Pass@1"
node scripts/login.js --email [REDACTED_SECRET] --password "Pass@1"
node scripts/get-me.js --pretty
```

### WhatsApp Connection

| Script | Description |
|--------|-------------|
| `get-connection-status.js` | Check Meta registration and subscription status |
| `refresh-connection.js` | Force re-sync with Meta's WhatsApp Business API |

```bash
node scripts/get-connection-status.js --pretty
node scripts/refresh-connection.js --pretty
```

> **Note:** Initial WhatsApp connection (QR scan / embedded signup) must be done once via the Notifyer console UI. These scripts manage the connection after it is established.

### Plans & Usage

| Script | Description |
|--------|-------------|
| `list-plans.js` | List all subscription plans and pricing tiers |
| `get-user-plan.js` | Current subscription status and usage limits |

```bash
node scripts/list-plans.js --pretty
node scripts/get-user-plan.js --pretty
```

### Team & Roles

| Script | Description |
|--------|-------------|
| `list-members.js` | List all team members |
| `invite-member.js` | Create a team member account |
| `update-member.js` | Update role, labels, name, or password |
| `remove-member.js` | Permanently remove a team member |

```bash
node scripts/list-members.js --labels --pretty
node scripts/invite-member.js --name "John" --email [REDACTED_SECRET] --password "Pass@1" --role "Team Member" --labels "Sales,Support"
node scripts/update-member.js --id <uuid> --role Admin
node scripts/remove-member.js --id <uuid> --confirm
```

Roles: `Admin` | `Team Member (All Labels)` | `Team Member`

### Labels

| Script | Description |
|--------|-------------|
| `list-labels.js` | List workspace labels |
| `create-label.js` | Create a label with optional auto-assignment keywords |
| `update-label-keywords.js` | Update label name or keywords |
| `delete-label.js` | Permanently delete a label |

```bash
node scripts/create-label.js --label "Sales" --keywords "buy,pricing,quote"
node scripts/update-label-keywords.js --id 5 --add "urgent"
node scripts/delete-label.js --id 5 --confirm
```

### Developer API Key

| Script | Description |
|--------|-------------|
| `get-api-key.js` | Retrieve the Developer API key for Make/Zapier/n8n |

```bash
node scripts/get-api-key.js --pretty
```

> Pro or Agency plan required. Use the **"Notifyer Systems"** module in Make/Zapier.

---

## Script Reference — automate-notifyer

```bash
cd skills/automate-notifyer
node scripts/<script>.js [flags]
```

### Templates

| Script | Description |
|--------|-------------|
| `list-templates.js` | List all approved and pending templates |
| `get-template.js` | Fetch a single template by name or ID |
| `create-template.js` | Submit a template for Meta approval |
| `delete-template.js` | Permanently delete a template from Notifyer + Meta (`--confirm` required) |

```bash
node scripts/list-templates.js --pretty
node scripts/create-template.js --name "order_confirm" --category UTILITY --body "Hello {{1}}, your order {{2}} is confirmed." --variables 2
node scripts/delete-template.js --id 987654321 --confirm   # whatsapp_template_id from list-templates.js
```

> **Side effect:** `list-templates.js` auto-syncs PENDING templates from Meta on each call.

### AI Bots

| Script | Description |
|--------|-------------|
| `list-bots.js` | List all AI bots |
| `get-bot.js` | Fetch a single bot by ID |
| `create-bot.js` | Create an AI bot (backed by OpenAI Assistant) |
| `update-bot.js` | Update bot fields (name, tone, knowledge base, delay, etc.) |
| `set-default-bot.js` | Set a bot as the workspace default |
| `delete-bot.js` | Permanently delete a bot + its OpenAI Assistant (`--confirm` required) |

```bash
node scripts/list-bots.js --pretty
node scripts/create-bot.js --name "Support Bot" --mission "Help users with support queries." --tone "Friendly" --default
node scripts/update-bot.js --id 12 --tone "Professional" --delay 5
node scripts/set-default-bot.js --id 12
node scripts/delete-bot.js --id 12 --confirm
```

### Broadcasts

| Script | Description |
|--------|-------------|
| `list-broadcasts.js` | List broadcasts by status (upcoming/previous/ongoing) |
| `get-broadcast.js` | Fetch a single broadcast by ID or name |
| `create-broadcast.js` | Create and schedule a broadcast (3-step flow) |
| `delete-broadcast.js` | Cancel/delete an upcoming broadcast (`--confirm` required) |

```bash
node scripts/list-broadcasts.js --require upcoming --pretty
node scripts/create-broadcast.js \
  --name "Jan Promo" \
  --template tmpl_abc123 \
  --recipients recipients.csv \
  --schedule "25/01/2025 10:00"
node scripts/delete-broadcast.js --id 5 --confirm
```

### Analytics

| Script | Description |
|--------|-------------|
| `get-message-analytics.js` | Summary stats — sent, delivered, read rates |
| `get-message-logs.js` | Per-message logs with phone/type filter |

```bash
node scripts/get-message-analytics.js --from "01/01/2025" --to "31/01/2025" --pretty
node scripts/get-message-logs.js --phone 14155550123 --pretty
```

### Webhooks

| Script | Description |
|--------|-------------|
| `list-webhooks.js` | List dev or IO webhooks (`--type dev\|io`) |
| `create-webhook.js` | Create a webhook with triggers and optional HMAC signature |
| `update-webhook.js` | Update URL, status, or triggers (fetch-then-patch) |
| `delete-webhook.js` | Delete a webhook (`--confirm` required) |

```bash
node scripts/list-webhooks.js --type dev --pretty
node scripts/create-webhook.js --type dev --url https://my.app/webhook --incoming --outgoing --signature
node scripts/delete-webhook.js --type dev --id 3 --confirm
```

---

## Script Reference — chat-notifyer

```bash
cd skills/chat-notifyer
node scripts/<script>.js [flags]
```

### Recipients

| Script | Description |
|--------|-------------|
| `list-recipients.js` | List/search all conversation contacts |
| `get-recipient.js` | Fetch a single recipient by phone number |
| `filter-recipients-by-label.js` | List recipients filtered by label(s) |
| `update-recipient.js` | Update recipient display name |

```bash
node scripts/list-recipients.js --search "John" --pretty
node scripts/get-recipient.js --phone 14155550123 --pretty
node scripts/filter-recipients-by-label.js --labels "Support" --status unread --pretty
node scripts/update-recipient.js --phone 14155550123 --name "John Doe"
```

### Messaging

| Script | Description |
|--------|-------------|
| `send-text.js` | Send a free-text message (24h window required) |
| `send-template.js` | Send a template message (works any time) |
| `send-attachment.js` | Upload a file and send it as a media message |

```bash
# Check 24h window first:
node scripts/get-recipient.js --phone 14155550123 --pretty

# Send text (window open) — script auto-checks the 24h window and refuses if closed:
node scripts/send-text.js --phone 14155550123 --text "Hello! How can I help?"

# Send template (works even if window closed) — use --name (easiest) or --template:
node scripts/send-template.js --phone 14155550123 --name order_confirm \
  --variables '{"body1":"John","body2":"#12345"}'

# List available templates to find the right name:
node scripts/send-template.js --list

# Preview a template send without sending (--dry-run):
node scripts/send-template.js --phone 14155550123 --name order_confirm --dry-run

# Send attachment:
node scripts/send-attachment.js --phone 14155550123 --file ./invoice.pdf --pretty
```

> **24h Window Rule:** WhatsApp only allows free-text and attachment messages within 24 hours of the recipient's last inbound message. `send-text.js` enforces this automatically and exits with a clear error if the window is closed. Use `send-template.js` when the window is closed.

### Labels

| Script | Description |
|--------|-------------|
| `assign-label.js` | Assign a label to a recipient conversation |
| `remove-label.js` | Remove a label from a recipient conversation |

```bash
node scripts/assign-label.js --phone 14155550123 --label "Support" --pretty
node scripts/remove-label.js --phone 14155550123 --label "Support" --pretty
```

### AI Handoff

| Script | Description |
|--------|-------------|
| `set-handoff.js` | Toggle AI bot ↔ human agent mode |
| `assign-bot.js` | Assign a specific AI bot to a recipient |
| `list-bots.js` | List all AI bots (get IDs for assign-bot.js) |

```bash
# Human takes over:
node scripts/set-handoff.js --phone 14155550123 --mode human --pretty

# Return to bot:
node scripts/set-handoff.js --phone 14155550123 --mode bot

# Assign a specific bot:
node scripts/list-bots.js --pretty
node scripts/assign-bot.js --phone 14155550123 --bot-id 5
```

### Scheduled Messages

| Script | Description |
|--------|-------------|
| `list-scheduled.js` | View all queued scheduled messages |
| `delete-scheduled.js` | Cancel a scheduled message |

```bash
# Schedule a message (works with send-text, send-template, send-attachment):
node scripts/send-template.js --phone 14155550123 --template tmpl_abc123 --schedule "25/01/2025 14:00"

# Manage scheduled messages:
node scripts/list-scheduled.js --pretty
node scripts/delete-scheduled.js --id 7 --confirm
```

### Notes & Conversation History

| Script | Description |
|--------|-------------|
| `add-note.js` | Set, append, or clear the manual note on a recipient |
| `get-notes.js` | Read manual + AI-generated notes |
| `get-conversation.js` | Read full bidirectional chat thread (sent + received messages) |
| `get-conversation-log.js` | Read outbound-only send history with delivery status |

```bash
node scripts/get-notes.js --phone 14155550123 --pretty
node scripts/add-note.js --phone 14155550123 --note "VIP — apply 15% discount"
node scripts/add-note.js --phone 14155550123 --append "Follow up on 15 Feb"
node scripts/get-conversation.js --phone 14155550123 --pretty          # full thread (both sides)
node scripts/get-conversation-log.js --phone 14155550123 --all --pretty # outbound only
```

---

## Use Cases — What AI Agents Can Do

### Workspace Onboarding Automation

An agent can fully onboard a new Notifyer workspace:

1. `create-account.js` — create the workspace
2. `login.js` — get the auth token
3. `get-user-plan.js` — verify subscription is active
4. `get-connection-status.js` — confirm WhatsApp is connected
5. `create-label.js` — create labels (`Sales`, `Support`, `VIP`)
6. `invite-member.js` — add team members
7. `update-member.js --labels` — assign labels to each member
8. `get-api-key.js` — retrieve the developer key for Make/Zapier/n8n

A single prompt like *"Set up a Notifyer workspace for Acme Corp with a Sales team of 3 agents"* can drive all 8 steps.

---

### Template Lifecycle Management

```bash
# Create a template
node scripts/create-template.js --name "order_shipped" --category UTILITY \
  --body "Hi {{1}}, your order {{2}} has shipped!" --variables 2

# Monitor approval status
node scripts/list-templates.js --pretty   # auto-syncs PENDING status from Meta

# Use the approved template in a broadcast
node scripts/create-broadcast.js --template order_shipped --recipients customers.csv \
  --schedule "25/01/2025 09:00"
```

---

### Full Chat Operation Workflow

```bash
# 1. Check who needs attention (unread, high-priority label)
node scripts/filter-recipients-by-label.js --labels "VIP" --status unread --pretty

# 2. Get full context on a contact
node scripts/get-recipient.js --phone 14155550123 --pretty
node scripts/get-notes.js --phone 14155550123 --pretty
node scripts/get-conversation.js --phone 14155550123 --pretty          # full thread (sent + received)
node scripts/get-conversation-log.js --phone 14155550123 --pretty      # outbound delivery log

# 3. Take over from the bot and respond
node scripts/set-handoff.js --phone 14155550123 --mode human
node scripts/send-text.js --phone 14155550123 --text "Hi John! I'm looking into this now."

# 4. Update notes with context
node scripts/add-note.js --phone 14155550123 --note "Escalated — awaiting refund approval"

# 5. Assign the right label and return to bot
node scripts/assign-label.js --phone 14155550123 --label "Escalated"
node scripts/set-handoff.js --phone 14155550123 --mode bot
```

---

### AI-Driven Customer Support Triage

An agent can continuously process incoming conversations:

1. `list-recipients.js --status unread` — find new conversations
2. `get-recipient.js` — check 24h window and AI mode status
3. `get-notes.js` — load context (manual + AI notes)
4. `get-conversation.js` — read full bidirectional thread (sent + received)
5. `get-conversation-log.js` — read outbound delivery log
6. Decide: escalate, respond, or route
7. `assign-label.js` — categorise the conversation
8. `send-template.js` — send an acknowledgement if window is closed
9. `set-handoff.js --mode human` — escalate complex cases

---

### Broadcast Campaign Management

```bash
# Plan and schedule a campaign
node scripts/list-templates.js --pretty         # pick an approved template
node scripts/get-user-plan.js --pretty          # check usage limits
node scripts/create-broadcast.js \
  --name "Feb Promo" \
  --template tmpl_promo \
  --recipients customers.csv \
  --schedule "01/02/2025 10:00"

# Monitor in progress
node scripts/list-broadcasts.js --require ongoing --pretty

# Review results
node scripts/get-message-analytics.js --from "01/02/2025" --to "28/02/2025" --pretty
node scripts/get-message-logs.js --filter broadcast --pretty
```

---

### Pre-flight Checks Before Sending

Before any automation sends a message, an agent should gate on:

```bash
# One-command health check (validates token, connection, and plan simultaneously):
cd skills/setup-notifyer
node scripts/doctor.js --pretty

# Then check the specific recipient's 24h window:
cd ../chat-notifyer
node scripts/get-recipient.js --phone <n> --pretty  # 24h window open?
```

This prevents failed sends and surfaces issues (expired token, stale connection, expired subscription, closed window) proactively. The `doctor.js` output includes a `fix` hint for every failing check.

---

### Webhook + Automation Integration

```bash
# Set up an outgoing webhook to receive message events
node scripts/create-webhook.js --type dev --url https://my.app/hook --incoming --outgoing --signature

# Set up an IO webhook for Make/Zapier/n8n event-driven automations
node scripts/create-webhook.js --type io --url https://hook.eu2.make.com/xyz --outgoing --schedule-activity

# Monitor configured webhooks
node scripts/list-webhooks.js --type dev --pretty
node scripts/list-webhooks.js --type io --pretty
```

---

### Workspace Health Check

```bash
# Quick pre-flight (token + connection + plan in one shot):
cd skills/setup-notifyer
node scripts/doctor.js --pretty

# Deep workspace audit:
node scripts/get-me.js --pretty                  # identity
node scripts/get-connection-status.js --pretty   # WhatsApp live + degraded check
node scripts/get-user-plan.js --pretty           # plan active, usage OK?
node scripts/list-members.js --pretty            # team structure
node scripts/list-labels.js --pretty             # label inventory
cd ../automate-notifyer
node scripts/list-bots.js --pretty               # AI bots active?
node scripts/list-webhooks.js --type dev --pretty # webhooks configured?
```

All outputs are structured JSON — trivially pipeable to monitoring tools or dashboards.

---

## Limitations

### Hard Limits (cannot be scripted by design)

| Limitation | Details |
|-----------|---------|
| **WhatsApp initial connection is browser-only** | The QR scan / embedded signup (Meta OAuth flow) requires a browser. An agent can check and refresh an existing connection, but cannot perform the first-time WABA setup. Direct users to [console.notifyer-systems.com](https://console.notifyer-systems.com). |
| **Plan upgrades/downgrades require the console UI** | Stripe checkout requires a browser redirect. `list-plans.js` and `get-user-plan.js` are read-only. |
| **Template approval is Meta's process** | `create-template.js` submits to Meta. Approval takes 24–72 hours and cannot be expedited via API. Templates must reach `APPROVED` status before use. |
| **Templates cannot be edited after approval** | Create a new template with a different name. There is no update-template endpoint. |
| **Deleting a broadcast cannot recall messages already sent** | `delete-broadcast.js` stops future batches but does not undo messages already dispatched. Only use it on upcoming (not yet started) broadcasts. |
| **AI bot requires OpenAI API key in Notifyer settings** | `create-bot.js` fails if the workspace has no valid OpenAI key — set this in the console UI. |
| **Password recovery is browser-only** | No password reset API endpoint. |

### API & Messaging Constraints

| Limitation | Details |
|-----------|---------|
| **24h window enforced by WhatsApp server-side** | Scripts cannot bypass it. Use `send-template.js` when the window is closed. |
| **Outbound-only message history** | `get-conversation-log.js` shows messages sent by Notifyer. Inbound messages from customers are not in the log — view them in [chat.notifyer-systems.com](https://chat.notifyer-systems.com). |
| **No bulk send from chat scripts** | For bulk messaging, use `automate-notifyer/create-broadcast.js`. |
| **`note_auto` (AI note) is read-only** | AI-generated notes are written by Notifyer's internal AI — cannot be set or cleared via API. |
| **`DELETE /webhook/dev/:id` has no server-side user auth in Xano** | `delete-webhook.js` mitigates this with an ownership check (authenticated `GET` before `DELETE`). The raw API endpoint remains public — a Xano-side fix is still recommended. |
| **IO webhook ID is a text UUID** | Unlike integer dev webhook IDs. Always treat IO webhook IDs as strings. |
| **No token refresh endpoint** | When the JWT expires (HTTP 401), re-run `login.js` for a fresh token. |

### Role Constraints

| Limitation | Details |
|-----------|---------|
| **Super Admin role is immutable** | Cannot be modified, demoted, or deleted by any script. |
| **Email is immutable after account creation** | Team member email addresses cannot be changed. |
| **Team Member token restricts recipient visibility** | Xano server-side filters recipients to the member's assigned label scope — scripts cannot override this. |
| **Label deletion does not cascade** | When a label is deleted, members with that label assigned are NOT automatically updated. Run `update-member.js --labels` to reassign them. |
| **API key requires Pro or Agency plan** | Basic (Bulk Message) accounts cannot use the Developer API or Make/Zapier/n8n. |

---

## Repository Structure

```
whatsapp-business-agent-skills/
├── README.md
└── skills/
    ├── setup-notifyer/                     ← Phase 1: workspace setup & infra
    │   ├── SKILL.md                        ← Agent entrypoint
    │   ├── package.json
    │   ├── scripts/
    │   │   ├── lib/                        ← Shared: notifyer-api.js, args.js, result.js
    │   │   ├── doctor.js
    │   │   ├── create-account.js
    │   │   ├── login.js
    │   │   ├── get-me.js
    │   │   ├── get-connection-status.js
    │   │   ├── refresh-connection.js
    │   │   ├── list-plans.js
    │   │   ├── get-user-plan.js
    │   │   ├── list-members.js
    │   │   ├── invite-member.js
    │   │   ├── update-member.js
    │   │   ├── remove-member.js
    │   │   ├── list-labels.js
    │   │   ├── create-label.js
    │   │   ├── update-label-keywords.js
    │   │   ├── delete-label.js
    │   │   └── get-api-key.js
    │   ├── references/                     ← API docs per feature area
    │   └── assets/                         ← Example payloads
    │
    ├── automate-notifyer/                  ← Phase 2: automation & integrations
    │   ├── SKILL.md
    │   ├── package.json
    │   ├── scripts/
    │   │   ├── lib/
    │   │   ├── list-templates.js
    │   │   ├── get-template.js
    │   │   ├── create-template.js
    │   │   ├── delete-template.js
    │   │   ├── list-bots.js
    │   │   ├── get-bot.js
    │   │   ├── create-bot.js
    │   │   ├── update-bot.js
    │   │   ├── delete-bot.js
    │   │   ├── set-default-bot.js
    │   │   ├── list-broadcasts.js
    │   │   ├── get-broadcast.js
    │   │   ├── create-broadcast.js
    │   │   ├── delete-broadcast.js
    │   │   ├── get-message-analytics.js
    │   │   ├── get-message-logs.js
    │   │   ├── list-webhooks.js
    │   │   ├── create-webhook.js
    │   │   ├── update-webhook.js
    │   │   └── delete-webhook.js
    │   ├── references/
    │   └── assets/
    │
    └── chat-notifyer/                      ← Phase 3: live chat operations
        ├── SKILL.md
        ├── package.json
        ├── scripts/
        │   ├── lib/
        │   ├── list-recipients.js
        │   ├── get-recipient.js
        │   ├── filter-recipients-by-label.js
        │   ├── update-recipient.js
        │   ├── send-text.js
        │   ├── send-template.js
        │   ├── send-attachment.js
        │   ├── assign-label.js
        │   ├── remove-label.js
        │   ├── set-handoff.js
        │   ├── assign-bot.js
        │   ├── list-bots.js
        │   ├── list-scheduled.js
        │   ├── delete-scheduled.js
        │   ├── add-note.js
        │   ├── get-notes.js
        │   ├── get-conversation.js
        │   └── get-conversation-log.js
        ├── references/
        └── assets/
```

---

## AgentSkills Format

This package follows the [AgentSkills open standard](https://agentskills.io/specification). Each skill is a directory with a `SKILL.md` file at the root. The frontmatter declares metadata; the body provides instructions, how-tos, rules, and a file map.

```
my-skill/
├── SKILL.md          # Required: instructions + metadata
├── scripts/          # Executable wrappers around API endpoints
├── references/       # Detailed API documentation
└── assets/           # Example payloads and fixtures
```

Skills use **progressive disclosure**: agents load only the skill name and description at startup. The full `SKILL.md` — and any referenced scripts or docs — is loaded only when the agent determines it is relevant to the current task.

Compatible agents (partial list): OpenClaw, Cursor, Claude Code, GitHub Copilot, VS Code, Gemini CLI, Amp, Roo Code, Junie, OpenHands, Mux, Goose, Letta, Firebender, Factory, Piebald, TRAE, Spring AI, and [many more](https://agentskills.io).

---

## Testing

A complete test suite covering all 57 scripts is available in [`TESTING.md`](TESTING.md).

It includes:
- Unit test prompts for every individual script
- Integration workflow tests (onboarding, broadcast campaigns, chat triage, etc.)
- Error & edge case tests (expired token, 24h window, degraded connection, destroy guards)
- A `smoke-test.sh` script for quick read-only validation of all live endpoints

---

## Learn More

| Resource | Link |
|----------|------|
| Notifyer Console | [console.notifyer-systems.com](https://console.notifyer-systems.com) |
| Notifyer Chat | [chat.notifyer-systems.com](https://chat.notifyer-systems.com) |
| Notifyer Documentation | [docs.whatsable.app](https://docs.whatsable.app) |
| AgentSkills specification | [agentskills.io/specification](https://agentskills.io/specification) |
| WhatsAble | [notifyer-systems.com](https://notifyer-systems.com) |

---

*Proprietary — © WhatsAble. All rights reserved.*


path: skills/automate-notifyer/PaxHeader/clawhub.json
content: 30 mtime=1776964078.743360653
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/clawhub.json
content: {
  "name": "automate-notifyer",
  "tagline": "Build WhatsApp automation — templates, bots, broadcasts, webhooks",
  "description": "Build automation infrastructure on a Notifyer by WhatsAble account — manage WhatsApp message templates, create and configure AI bots for automated chat handling, create and schedule bulk WhatsApp broadcast campaigns, retrieve messaging analytics and delivery logs, and manage developer webhooks (n8n / Make / Zapier integrations) and IO webhooks. Use this skill after setup-notifyer has been completed.",
  "category": "productivity",
  "tags": [
    "whatsapp",
    "notifyer",
    "whatsable",
    "messaging",
    "automation",
    "broadcasts",
    "templates",
    "webhooks",
    "bots",
    "analytics"
  ],
  "version": "0.3.0",
  "author": "whatsable",
  "license": "Proprietary",
  "homepage": "https://github.com/Whatsable/whatsapp-business-agent-skills",
  "repository": "https://github.com/Whatsable/whatsapp-business-agent-skills",
  "requires": {
    "node": ">=18"
  },
  "env": [
    {
      "name": "NOTIFYER_API_BASE_URL",
      "description": "Base URL for the Notifyer API (e.g. https://api.insightssystem.com)",
      "required": true
    },
    {
      "name": "NOTIFYER_API_TOKEN",
      "description": "JWT token obtained from setup-notifyer/login.js. Required for all API calls.",
      "required": true
    }
  ]
}


path: skills/automate-notifyer/PaxHeader/package.json
content: 30 mtime=1776964078.747051106
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/package.json
content: {
  "name": "automate-notifyer",
  "version": "0.3.0",
  "description": "Scripts for building automation infrastructure on Notifyer — templates, AI bots, and broadcasts",
  "type": "module",
  "scripts": {
    "check": "node --check scripts/list-templates.js && node --check scripts/get-template.js && node --check scripts/create-template.js && node --check scripts/delete-template.js && node --check scripts/list-bots.js && node --check scripts/get-bot.js && node --check scripts/create-bot.js && node --check scripts/update-bot.js && node --check scripts/delete-bot.js && node --check scripts/set-default-bot.js && node --check scripts/list-broadcasts.js && node --check scripts/get-broadcast.js && node --check scripts/create-broadcast.js && node --check scripts/delete-broadcast.js && node --check scripts/get-message-analytics.js && node --check scripts/get-message-logs.js && node --check scripts/list-webhooks.js && node --check scripts/create-webhook.js && node --check scripts/update-webhook.js && node --check scripts/delete-webhook.js && echo 'All scripts OK'",
    "update-filemap": "node ../../update-skill-filemap.mjs"
  },
  "engines": {
    "node": ">=18"
  },
  "dependencies": {}
}


path: skills/automate-notifyer/assets/PaxHeader/recipients-example.csv
content: 30 mtime=1775733966.599670029
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/assets/recipients-example.csv
content: phone_number
14155550101
14155550102
14155550103


path: skills/automate-notifyer/assets/PaxHeader/template-create-example.json
content: 30 mtime=1776964078.748245479
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/assets/template-create-example.json
content: {
  "_comment": "Example payloads for create-template.js — three common template types",

  "text_with_variables": {
    "_description": "node scripts/create-template.js --name order_confirmation --category MARKETING --body 'Hello {{1}}, your order #{{2}} has shipped.' --variables '{\"1\":\"John\",\"2\":\"12345\"}'",
    "name": "order_confirmation",
    "category": "MARKETING",
    "language": "en",
    "body": "Hello {{1}}, your order #{{2}} has shipped.",
    "variables": { "1": "John", "2": "12345" }
  },

  "image_with_body_and_buttons": {
    "_description": "node scripts/create-template.js --name promo_banner --category MARKETING --body 'Check out our sale, {{1}}!' --variables '{\"1\":\"Jane\"}' --type image --media-url 'https://example.com/banner.jpg' --buttons '[{\"type\":\"Quick Reply\",\"text\":\"Shop Now\"},{\"type\":\"Quick Reply\",\"text\":\"No Thanks\"}]'",
    "name": "promo_banner",
    "category": "MARKETING",
    "language": "en",
    "type": "image",
    "body": "Check out our sale, {{1}}!",
    "variables": { "1": "Jane" },
    "media_url": "https://example.com/banner.jpg",
    "buttons": [
      { "type": "Quick Reply", "text": "Shop Now" },
      { "type": "Quick Reply", "text": "No Thanks" }
    ],
    "_note": "media_url is auto-uploaded by the script — pass a publicly accessible URL"
  },

  "authentication": {
    "_description": "node scripts/create-template.js --name verify_otp --category AUTHENTICATION --expiry 10",
    "name": "verify_otp",
    "category": "AUTHENTICATION",
    "expiry": 10,
    "_note": "Do NOT pass --body for AUTHENTICATION templates. Xano auto-generates: 'Your verification code is {{1}}. Do not share this code. (Expires in 10 minutes.)'"
  }
}


path: skills/automate-notifyer/assets/PaxHeader/broadcast-create-example.json
content: 30 mtime=1776964078.748658478
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/assets/broadcast-create-example.json
content: {
  "_comment": "Example for create-broadcast.js — the 3-step broadcast flow",
  "_note": "create-broadcast.js orchestrates all 3 steps. Use this as a reference for the flags it accepts.",

  "smart_delivery": {
    "_description": "Batched broadcast with auto-pacing based on read rate",
    "cli": "node scripts/create-broadcast.js --name 'January Sale' --template-id 42 --test-phone '+14155550123' --recipients ./assets/recipients-example.csv --schedule '25/01/2025 14:00' --delivery-mode smart --delivery-size 4 --read-rate 95",
    "flags": {
      "--name": "January Sale",
      "--template-id": 42,
      "--test-phone": "+14155550123",
      "--recipients": "./assets/recipients-example.csv",
      "--schedule": "25/01/2025 14:00",
      "--delivery-mode": "smart",
      "--delivery-size": 4,
      "--read-rate": 95
    }
  },

  "regular_delivery_with_variables": {
    "_description": "Fixed-batch broadcast with per-recipient body variables in the CSV",
    "cli": "node scripts/create-broadcast.js --name 'Order Update' --template-id 42 --test-phone '+14155550123' --recipients ./recipients.csv --schedule '26/01/2025 09:00' --delivery-mode regular --delivery-size 10",
    "_note": "body1, body2, body3 columns in the CSV map to {{1}}, {{2}}, {{3}} in the template body per recipient"
  },

  "success_response_shape": {
    "broadcast_identifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "test": {
      "success": true,
      "broadcast_name": "January Sale",
      "template_id": 42
    },
    "recipients": {
      "unique_numbers": 3,
      "cost_of_broadcast": 0.045
    },
    "schedule": {
      "broadcast_name": "January Sale",
      "scheduled_at": "25/01/2025 14:00",
      "delivery_mode": "smart",
      "delivery_size": 4,
      "read_rate": 95
    }
  },

  "delivery_modes": {
    "smart": "Adaptive batching — waits for read_rate % of a batch to be read before sending the next. Safest for large lists.",
    "regular": "Fixed-size batches at a constant pace. Predictable but no read-rate feedback loop.",
    "risk": "No batching — all messages sent at once. Only for very small urgent lists. Can trigger Meta spam detection."
  },

  "csv_rules": [
    "phone_number column must NOT include + prefix — use 14155550101, not +14155550101",
    "Variable columns (body1, body2, body3) map to {{1}}, {{2}}, {{3}} in the template",
    "media column: public URL for per-recipient media (image/document/video templates)",
    "button_dynamic_url_value column: per-recipient URL suffix for CTA buttons",
    "Xano deduplicates phone numbers — duplicates in CSV are silently dropped",
    "Re-uploading CSV to same broadcast_identifier deletes the previous recipient list first"
  ]
}


path: skills/automate-notifyer/scripts/PaxHeader/get-bot.js
content: 30 mtime=1775565397.740777041
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/get-bot.js
content: #!/usr/bin/env node
/**
 * get-bot.js — Retrieve a single AI bot by its numeric ID.
 *
 * GET /api:Sc_sezER/ai_config/:id
 *
 * Usage:
 *   node scripts/get-bot.js --id 12
 *   node scripts/get-bot.js --id 12 --pretty
 *
 * Flags:
 *   --id <integer>   Notifyer bot ID (required)
 *   --pretty         Print human-readable summary to stderr  (optional)
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "id": 12,
 *       "bot_name": "Support Bot",
 *       "mission": "Help users resolve support issues quickly.",
 *       "system_prompt": "You are a helpful support agent...",
 *       "knowledge_base": "Our return policy is...",
 *       "tone": "Friendly",
 *       "delay": 3,
 *       "default": true,
 *       "notification": true,
 *       "human_trigger_keywords": ["agent", "human", "help"],
 *       "handoff_instruction": "I'll connect you with a human agent now.",
 *       "openai_assistant_id": "asst_abc123",
 *       "files_metadatas": [],
 *       "file_texts": {},
 *       "user_id": "uuid...",
 *       "created_at": "2024-01-01T00:00:00.000Z"
 *     }
 *   }
 *
 * Output (not found):
 *   { "ok": false, "error": "Bot with id 99 not found.", "status": 400 }
 *
 * Notes:
 *   - The `id` must be an integer — it maps to the `ai_config_id` path parameter.
 *   - Xano returns HTTP 400 "Precondition Failed" if no record exists for the given ID.
 *     The script surfaces this as a user-friendly "not found" error.
 *   - Does NOT filter by calling user — any authenticated user can look up any bot
 *     by ID in the workspace.
 *   - To find a bot by name use list-bots.js and filter client-side.
 *   - Uses console auth mode (Authorization: Bearer <token>).
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

function printSummary(bot) {
  const keywords = Array.isArray(bot.human_trigger_keywords)
    ? bot.human_trigger_keywords.join(", ") || "—"
    : "—";

  process.stderr.write(`\nAI Bot #${bot.id}\n`);
  process.stderr.write(`${"─".repeat(60)}\n`);
  process.stderr.write(`  Name         : ${bot.bot_name ?? "—"}\n`);
  process.stderr.write(`  Mission      : ${(bot.mission ?? "—").slice(0, 80)}\n`);
  process.stderr.write(`  Tone         : ${bot.tone ?? "—"}\n`);
  process.stderr.write(`  Delay        : ${bot.delay ?? 0}s\n`);
  process.stderr.write(`  Default      : ${bot.default ? "YES" : "NO"}\n`);
  process.stderr.write(`  Notification : ${bot.notification ? "ON" : "OFF"}\n`);
  process.stderr.write(`  Trigger kws  : ${keywords}\n`);
  process.stderr.write(
    `  Files        : ${Array.isArray(bot.files_metadatas) ? bot.files_metadatas.length : 0} file(s)\n`
  );
  process.stderr.write(`  OpenAI ID    : ${bot.openai_assistant_id ?? "—"}\n`);
  process.stderr.write(`  Created      : ${bot.created_at ?? "—"}\n`);
  process.stderr.write("\n");
}

async function main() {
  const flags = parseArgs();
  const idRaw = getFlag(flags, "id");
  const pretty = flags["pretty"] === true || flags["pretty"] === "";

  if (!idRaw) {
    printJson(err("--id <integer> is required. Example: node scripts/get-bot.js --id 12"));
    return;
  }

  const id = Number(idRaw);
  if (!Number.isInteger(id) || id <= 0) {
    printJson(err(`--id must be a positive integer, got: ${idRaw}`));
    return;
  }

  const config = loadConfig({ requireToken: true });

  const result = await requestJson(config, {
    method: "GET",
    path: `/api:Sc_sezER/ai_config/${id}`,
  });

  if (!result.ok) {
    // Xano fires Precondition (400) when the record is null → treat as not found
    if (result.status === 400) {
      printJson(err(`Bot with id ${id} not found.`, result.data, false, 400));
    } else {
      printJson(err(result.error, result.data, false, result.status));
    }
    return;
  }

  const bot = result.data;

  if (pretty) printSummary(bot);

  printJson(ok(bot));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/list-templates.js
content: 29 mtime=1775564153.50570266
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/list-templates.js
content: #!/usr/bin/env node
/**
 * list-templates.js — List all workspace WhatsApp message templates.
 *
 * GET /api:AFRA_QCy/templates_web
 *
 * Usage:
 *   node scripts/list-templates.js
 *   node scripts/list-templates.js --status approved
 *   node scripts/list-templates.js --category MARKETING
 *   node scripts/list-templates.js --type image
 *   node scripts/list-templates.js --pretty
 *
 * Flags:
 *   --status <value>      Filter by status: approved | pending | rejected  (optional)
 *   --category <value>    Filter by category: MARKETING | UTILITY | AUTHENTICATION  (optional)
 *   --type <value>        Filter by template type: text | image | document | video  (optional)
 *   --pretty              Print human-readable table to stderr  (optional)
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "templates": [
 *         {
 *           "id": "abc123",
 *           "name": "order_confirmation",
 *           "template_id": "tmpl_...",
 *           "whatsapp_template_id": 123456789,
 *           "category": "MARKETING",
 *           "type": "text",
 *           "body": "Hello {{1}}, your order #{{2}} is confirmed.",
 *           "language": "en",
 *           "status": "approved",
 *           "components": [...]
 *         }
 *       ],
 *       "count": 5
 *     }
 *   }
 *
 * Notes:
 *   - Returns ALL templates for the workspace (no server-side pagination).
 *   - Status values:
 *       "approved"  — ready to use in broadcasts and chat sends
 *       "pending"   — under Meta review (usually resolves within 45 seconds)
 *       "rejected"  — failed Meta review; cannot be used for sends; must recreate
 *   - SIDE EFFECT: Every call to this endpoint auto-syncs PENDING templates with Meta.
 *     Xano loops over PENDING templates, calls the Meta API for each one to get the
 *     current status, and saves any changes. This means the returned status is always
 *     up-to-date — you do NOT need to poll separately for status changes.
 *   - To get a single template by name or ID use get-template.js.
 *   - For broadcast-ready (approved-only) templates use:
 *       GET /api:AFRA_QCy/templates_broadcast_web
 *     The list-templates.js --status approved flag achieves the same client-side.
 *   - Uses console auth mode (Authorization: Bearer <token>).
 *   - There is also a developer-API variant: GET /api:AFRA_QCy/get_templates (uses raw
 *     api_key auth, same sync behavior). Not wrapped in a script — use list-templates.js.
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const STATUS_COLORS = { approved: "✓", pending: "…", rejected: "✗" };

function printSummary(templates) {
  process.stderr.write(`\nWorkspace Templates (${templates.length} total)\n`);
  process.stderr.write(`${"─".repeat(90)}\n`);
  process.stderr.write(
    `${"Name".padEnd(32)} ${"Category".padEnd(16)} ${"Type".padEnd(10)} ${"Lang".padEnd(6)} ${"Status".padEnd(12)}\n`
  );
  process.stderr.write(`${"─".repeat(90)}\n`);

  for (const t of templates) {
    const name = (t.name ?? "").slice(0, 31).padEnd(32);
    const cat = (t.category ?? "").slice(0, 15).padEnd(16);
    const type = (t.type ?? "").slice(0, 9).padEnd(10);
    const lang = (t.language ?? "").slice(0, 5).padEnd(6);
    const status = t.status ?? "";
    const icon = STATUS_COLORS[status.toLowerCase()] ?? "?";
    process.stderr.write(`${name} ${cat} ${type} ${lang} ${icon} ${status}\n`);
  }

  process.stderr.write("\n");
}

async function main() {
  const flags = parseArgs();
  const statusFilter = getFlag(flags, "status")?.toLowerCase();
  const categoryFilter = getFlag(flags, "category")?.toUpperCase();
  const typeFilter = getFlag(flags, "type")?.toLowerCase();
  const pretty = getBooleanFlag(flags, "pretty");

  const config = loadConfig({ requireToken: true });

  const result = await requestJson(config, {
    method: "GET",
    path: "/api:AFRA_QCy/templates_web",
  });

  if (!result.ok) {
    printJson(err(result.error, result.data, false, result.status));
  }

  let templates = Array.isArray(result.data) ? result.data : [];

  if (statusFilter) {
    templates = templates.filter(
      (t) => (t.status ?? "").toLowerCase() === statusFilter
    );
  }
  if (categoryFilter) {
    templates = templates.filter(
      (t) => (t.category ?? "").toUpperCase() === categoryFilter
    );
  }
  if (typeFilter) {
    templates = templates.filter(
      (t) => (t.type ?? "").toLowerCase() === typeFilter
    );
  }

  if (pretty) printSummary(templates);

  printJson(ok({ templates, count: templates.length }));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/list-webhooks.js
content: 30 mtime=1775664919.872254864
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/list-webhooks.js
content: #!/usr/bin/env node
/**
 * list-webhooks.js — List webhook endpoints.
 *
 * GET /api:qh9OQ3OW/webhook/dev           (--type dev, default)
 * GET /api:qh9OQ3OW/user/io/webhook       (--type io)
 *
 * Usage:
 *   node scripts/list-webhooks.js                    # dev webhooks (default)
 *   node scripts/list-webhooks.js --type io          # IO (incoming & outgoing) webhooks
 *   node scripts/list-webhooks.js --pretty
 *
 * Flags:
 *   --type dev|io   dev = developer/schedule webhooks for n8n, Make, Zapier (default)
 *                   io  = incoming & outgoing bidirectional webhooks
 *   --pretty        Print human-readable table to stderr
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "type": "dev",
 *       "webhooks": [
 *         {
 *           "id": 5,
 *           "webhooks": "https://hook.eu2.make.com/...",
 *           "status": true,
 *           "outgoing": true,
 *           "incoming": true,
 *           "schedule_activity": false,
 *           "waiting_duration": 0,
 *           "signature_secret": null,
 *           "created_at": 1706184000000
 *         }
 *       ],
 *       "count": 1
 *     }
 *   }
 *
 * IO webhook fields: id, webhooks (URL), is_active, created_at
 *
 * Notes:
 *   - Dev webhook endpoints ALL require Origin: https://console.notifyer-systems.com
 *     because Xano runs /cors_origin_console. The script sends this automatically.
 *   - IO webhook endpoints do NOT require a CORS header (no cors check in Xano).
 *   - Dev webhook raw response may use is_incoming_outgoing_enable or
 *     is_incoming_outgoing instead of outgoing/incoming — this script normalizes both.
 *   - Xano table for dev: zapier_make_webhooks.
 *   - Xano table for IO: webhook_incoming_and_outgoing.
 *   - Both endpoints return direct arrays (As Self response).
 *   - Uses console auth mode (Authorization: Bearer <token>).
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const CONSOLE_ORIGIN = "https://console.notifyer-systems.com";
const VALID_TYPES = ["dev", "io"];

function normalizeDev(raw) {
  return {
    id: raw.id,
    webhooks: raw.webhooks,
    status: typeof raw.status === "boolean" ? raw.status : Boolean(raw.status),
    outgoing:
      typeof raw.outgoing === "boolean"
        ? raw.outgoing
        : Boolean(
            raw.is_incoming_outgoing_enable ??
              raw.is_incoming_outgoing ??
              raw.outgoing
          ),
    incoming:
      typeof raw.incoming === "boolean"
        ? raw.incoming
        : Boolean(
            raw.is_incoming_outgoing_enable ??
              raw.is_incoming_outgoing ??
              raw.outgoing
          ),
    schedule_activity:
      typeof raw.schedule_activity === "boolean" ? raw.schedule_activity : false,
    waiting_duration:
      typeof raw.waiting_duration === "number" ? raw.waiting_duration : 0,
    signature_secret: raw.signature_secret ?? null,
    created_at: raw.created_at,
  };
}

function formatTimestamp(ms) {
  if (!ms) return "N/A";
  return new Date(ms).toLocaleString("en-GB", {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  });
}

function printDevSummary(webhooks) {
  process.stderr.write(`\nDev Webhooks (${webhooks.length} total)\n`);
  process.stderr.write(`${"─".repeat(110)}\n`);
  process.stderr.write(
    `${"ID".padEnd(5)} ${"Status".padEnd(9)} ${"Out".padEnd(5)} ${"In".padEnd(5)} ${"Sched".padEnd(6)} ${"URL".padEnd(55)} ${"Created".padEnd(18)}\n`
  );
  process.stderr.write(`${"─".repeat(110)}\n`);
  for (const w of webhooks) {
    const id = String(w.id ?? "").padEnd(5);
    const status = (w.status ? "Active" : "Off").padEnd(9);
    const out = (w.outgoing ? "✓" : "✗").padEnd(5);
    const inc = (w.incoming ? "✓" : "✗").padEnd(5);
    const sched = (w.schedule_activity ? "✓" : "✗").padEnd(6);
    const url = (w.webhooks ?? "").slice(0, 54).padEnd(55);
    const date = formatTimestamp(w.created_at).padEnd(18);
    process.stderr.write(`${id} ${status} ${out} ${inc} ${sched} ${url} ${date}\n`);
  }
  process.stderr.write("\n");
}

function printIoSummary(webhooks) {
  process.stderr.write(`\nIO Webhooks (${webhooks.length} total)\n`);
  process.stderr.write(`${"─".repeat(90)}\n`);
  process.stderr.write(
    `${"ID".padEnd(5)} ${"Active".padEnd(8)} ${"URL".padEnd(55)} ${"Created".padEnd(18)}\n`
  );
  process.stderr.write(`${"─".repeat(90)}\n`);
  for (const w of webhooks) {
    const id = String(w.id ?? "").padEnd(5);
    const active = (w.is_active ? "✓" : "✗").padEnd(8);
    const url = (w.webhooks ?? "").slice(0, 54).padEnd(55);
    const date = formatTimestamp(w.created_at).padEnd(18);
    process.stderr.write(`${id} ${active} ${url} ${date}\n`);
  }
  process.stderr.write("\n");
}

async function main() {
  const flags = parseArgs();
  const type = (getFlag(flags, "type") ?? "dev").toLowerCase();
  const pretty = getBooleanFlag(flags, "pretty");

  if (!VALID_TYPES.includes(type)) {
    printJson(err(`--type must be "dev" or "io". Got: "${type}"`));
    return;
  }

  const config = loadConfig({ requireToken: true });

  const isDev = type === "dev";
  const result = await requestJson(config, {
    method: "GET",
    path: isDev ? "/api:qh9OQ3OW/webhook/dev" : "/api:qh9OQ3OW/user/io/webhook",
    ...(isDev ? { extraHeaders: { Origin: CONSOLE_ORIGIN } } : {}),
  });

  if (!result.ok) {
    printJson(err(result.error, result.data, false, result.status));
    return;
  }

  const raw = Array.isArray(result.data) ? result.data : [];
  const webhooks = isDev ? raw.map(normalizeDev) : raw;

  if (pretty) {
    isDev ? printDevSummary(webhooks) : printIoSummary(webhooks);
  }

  printJson(ok({ type, webhooks, count: webhooks.length }));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/delete-template.js
content: 30 mtime=1776106095.186523155
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/delete-template.js
content: #!/usr/bin/env node
/**
 * delete-template.js — Delete a WhatsApp message template from Notifyer.
 *
 * DELETE /api:AFRA_QCy/templates/delete
 * Body: { whatsapp_template_id: <integer> }
 *
 * IMPORTANT — This is PERMANENT and IRREVERSIBLE:
 *   - Deletes the template record from Notifyer's database.
 *   - Submits a deletion request to the Meta WhatsApp API.
 *   - Meta propagates the deletion; the template name cannot be reused
 *     for 30 days per Meta's policy.
 *   - Approved templates count against your workspace quota; deletion
 *     frees up one slot.
 *
 * Usage:
 *   node scripts/delete-template.js --id 1234567890 --confirm
 *   node scripts/delete-template.js --id 1234567890 --confirm --pretty
 *
 * Required Flags:
 *   --id <integer>   The whatsapp_template_id (integer) from list-templates.js.
 *                    NOT the template name — use the whatsapp_template_id field.
 *   --confirm        Required safety flag to prevent accidental deletion.
 *
 * Optional Flags:
 *   --pretty         Print deletion confirmation to stderr.
 *
 * How to get the whatsapp_template_id:
 *   node scripts/list-templates.js --pretty
 *   → Look for the whatsapp_template_id field (numeric, e.g. 987654321).
 *     Do NOT use the template_id string (e.g. "tmpl_...").
 *
 * Output (success):
 *   { "ok": true, "data": { "deleted": true, "whatsapp_template_id": 1234567890 } }
 *
 * Output (not found):
 *   { "ok": false, "error": "Template with whatsapp_template_id 123 not found or already deleted." }
 *
 * Output (in-use / Meta rejected):
 *   { "ok": false, "error": "...", "blocked": true, "data": { ... } }
 *
 * Note: Broadcasted templates cannot be deleted while a broadcast is ongoing.
 *   Wait for the broadcast to complete before deleting the template.
 *
 * Auth: Authorization: Bearer <token> (console auth mode).
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js, must be Super Admin or Admin)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

async function main() {
  const flags = parseArgs();
  const id = getNumberFlag(flags, "id");
  const confirm = getBooleanFlag(flags, "confirm");
  const pretty = getBooleanFlag(flags, "pretty");

  if (id === null || id === undefined) {
    printJson(err(
      "--id is required. Provide the whatsapp_template_id (integer) from list-templates.js.\n" +
      "  Run: node scripts/list-templates.js --pretty"
    ));
    return;
  }
  if (!confirm) {
    printJson(err(
      "--confirm is required. Template deletion is permanent and cannot be undone.\n" +
      "  Meta will prevent reuse of the template name for 30 days.\n" +
      "  Add --confirm to proceed."
    ));
    return;
  }

  const config = loadConfig({ requireToken: true });

  // First, fetch the template to check its status
  const listResult = await requestJson(config, {
    method: "GET",
    path: "/api:AFRA_QCy/templates_web",
  });

  if (!listResult.ok) {
    printJson(err("Failed to fetch templates list", listResult.data, false));
    return;
  }

  // API returns array directly in data field, not data.templates
  const templates = Array.isArray(listResult.data) ? listResult.data : [];
  const template = templates.find(t => t.whatsapp_template_id === Number(id));

  if (!template) {
    printJson(err(
      `Template with whatsapp_template_id ${id} not found.`,
      null,
      false
    ));
    return;
  }

  // Check if template is REJECTED
  if (template.status === "REJECTED") {
    if (pretty) {
      process.stderr.write(`\n⚠️  Cannot delete REJECTED template "${template.name}" (ID: ${id})\n\n`);
      process.stderr.write(`  Why: Meta API returns 400 error for REJECTED templates because they\n`);
      process.stderr.write(`       no longer exist on Meta's side. Notifyer's backend doesn't handle\n`);
      process.stderr.write(`       this case and leaves the database record intact.\n\n`);
      process.stderr.write(`  Options:\n`);
      process.stderr.write(`    1. Contact Notifyer support to manually remove from database\n`);
      process.stderr.write(`    2. Ignore it — REJECTED templates can't be used anyway\n\n`);
      process.stderr.write(`  Note: Only APPROVED templates can be deleted through the API.\n\n`);
    }
    printJson(err(
      `Cannot delete REJECTED template. Only APPROVED templates can be deleted. ` +
      `Template "${template.name}" (ID: ${id}) was rejected by Meta and no longer exists ` +
      `on Meta's side, so the API cannot process the deletion. Contact Notifyer support ` +
      `to remove this record from the database.`,
      { 
        template_name: template.name,
        whatsapp_template_id: template.whatsapp_template_id,
        status: template.status,
        category: template.category,
        can_delete: false,
        reason: "REJECTED templates cannot be deleted via API"
      },
      false
    ));
    return;
  }

  // Proceed with deletion for APPROVED templates
  const result = await requestJson(config, {
    method: "DELETE",
    path: "/api:AFRA_QCy/templates/delete",
    body: { whatsapp_template_id: Number(id) },
  });

  if (!result.ok) {
    if (result.status === 404) {
      printJson(err(
        `Template with whatsapp_template_id ${id} not found or already deleted.`,
        null,
        false
      ));
    } else {
      printJson(err(result.error, result.data, true, result.status));
    }
    return;
  }

  // Check if deletion actually succeeded (backend may return ok: true but success: false)
  if (result.data && result.data.success === false) {
    if (pretty) {
      process.stderr.write(`\n❌ Template deletion failed\n\n`);
      process.stderr.write(`  Template: ${template.name} (ID: ${id})\n`);
      process.stderr.write(`  Status: ${template.status}\n`);
      process.stderr.write(`  Error: ${result.data.result || "Unknown error"}\n\n`);
      if (template.status === "REJECTED") {
        process.stderr.write(`  Note: REJECTED templates cannot be deleted via API.\n\n`);
      }
    }
    printJson(err(
      `Template deletion failed: ${result.data.result || "Unknown error"}`,
      result.data,
      false
    ));
    return;
  }

  if (pretty) {
    process.stderr.write(`\n✓ Template ${template.name} (ID: ${id}) deleted successfully!\n`);
    process.stderr.write(`  Status: ${template.status}\n`);
    process.stderr.write(`  Note: Meta blocks reuse of this template name for 30 days.\n\n`);
  }

  printJson(ok({ deleted: true, whatsapp_template_id: Number(id), template_name: template.name }));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/update-webhook.js
content: 30 mtime=1775664998.834113343
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/update-webhook.js
content: #!/usr/bin/env node
/**
 * update-webhook.js — Update an existing webhook (URL, status, triggers).
 *
 * PATCH /api:qh9OQ3OW/webhook/dev/{id}         (--type dev, default)
 * PATCH /api:qh9OQ3OW/user/io/webhook?id=<id>  (--type io, id is text/string)
 *
 * Strategy: fetch-then-patch.
 *   Calls GET first to load the current record, merges your flag overrides,
 *   then sends the PATCH. This is required because Xano's "Add Or Edit Record"
 *   function expects a complete record object.
 *
 * Usage:
 *   # Dev webhook updates:
 *   node scripts/update-webhook.js --id 5 --url "https://new-endpoint.com/wh"
 *   node scripts/update-webhook.js --id 5 --status false
 *   node scripts/update-webhook.js --id 5 --no-outgoing --no-incoming
 *   node scripts/update-webhook.js --id 5 --schedule-activity --waiting-duration 1800
 *   node scripts/update-webhook.js --id 5 --no-schedule-activity
 *
 *   # IO webhook updates (id is a string):
 *   node scripts/update-webhook.js --type io --id "abc123" --url "https://new-endpoint.com"
 *   node scripts/update-webhook.js --type io --id "abc123" --status false
 *
 * Required Flags:
 *   --id <id>                    Webhook ID to update
 *                                Dev: integer. IO: string/text.
 *
 * Dev-specific Update Flags (ignored for --type io):
 *   --outgoing / --no-outgoing   Toggle outgoing message trigger
 *   --incoming / --no-incoming   Toggle incoming message trigger
 *   --schedule-activity / --no-schedule-activity   Toggle schedule trigger
 *   --waiting-duration <secs>    Update waiting duration in seconds
 *
 * Shared Update Flags:
 *   --url <url>                  Update the webhook endpoint URL
 *   --status true|false          Enable or disable the webhook
 *   --type dev|io                Webhook type (default: dev)
 *   --pretty                     Print summary to stderr
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": { ...updated webhook record }
 *   }
 *
 * Notes:
 *   - Dev webhook: PATCH uses id as a path parameter (PATCH /webhook/dev/:id).
 *   - IO webhook: PATCH sends id as a body field (Xano uses it for lookup).
 *     The id field for IO webhooks is TEXT type in Xano — do not cast to integer.
 *   - Dev PATCH only checks cors_origin_console (CORS), no user auth step.
 *     However, the script sends Authorization: Bearer as it is still an
 *     authenticated zone in practice (CORS check validates console session).
 *   - Dev webhook PATCH also accepts signature_secret field; use create-webhook.js
 *     with --signature to generate a new secret on a new webhook instead.
 *   - IO PATCH inputs: id (text), webhook (text — not "webhooks"), status (bool).
 *
 * CORS:
 *   Dev webhook endpoints run /cors_origin_console. The script sends
 *   Origin: https://console.notifyer-systems.com automatically.
 *   IO webhook endpoints do NOT require a CORS header.
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const CONSOLE_ORIGIN = "https://console.notifyer-systems.com";
const VALID_TYPES = ["dev", "io"];

function hasFlag(flags, name) {
  return flags[name] !== undefined;
}

async function main() {
  const flags = parseArgs();
  const type = (getFlag(flags, "type") ?? "dev").toLowerCase();
  const pretty = getBooleanFlag(flags, "pretty");
  const idRaw = getFlag(flags, "id");

  if (!VALID_TYPES.includes(type)) {
    printJson(err(`--type must be "dev" or "io". Got: "${type}"`));
    return;
  }

  if (!idRaw) {
    printJson(err("--id is required."));
    return;
  }

  const config = loadConfig({ requireToken: true });

  if (type === "dev") {
    const id = parseInt(idRaw, 10);
    if (isNaN(id)) {
      printJson(err("--id must be an integer for dev webhooks."));
      return;
    }

    // Fetch current state
    const listResult = await requestJson(config, {
      method: "GET",
      path: "/api:qh9OQ3OW/webhook/dev",
      extraHeaders: { Origin: CONSOLE_ORIGIN },
    });

    if (!listResult.ok) {
      printJson(err(`Failed to fetch current webhooks: ${listResult.error}`, listResult.data));
      return;
    }

    const all = Array.isArray(listResult.data) ? listResult.data : [];
    const current = all.find((w) => w.id === id);
    if (!current) {
      printJson(err(`Dev webhook with id ${id} not found. Use list-webhooks.js to see available webhooks.`));
      return;
    }

    // Merge overrides
    const merged = {
      id,
      webhooks: hasFlag(flags, "url") ? getFlag(flags, "url") : current.webhooks,
      status: hasFlag(flags, "status")
        ? getFlag(flags, "status") !== "false"
        : Boolean(current.status),
      outgoing: hasFlag(flags, "outgoing")
        ? getBooleanFlag(flags, "outgoing")
        : Boolean(current.outgoing ?? current.is_incoming_outgoing_enable ?? false),
      incoming: hasFlag(flags, "incoming")
        ? getBooleanFlag(flags, "incoming")
        : Boolean(current.incoming ?? current.is_incoming_outgoing_enable ?? false),
      schedule_activity: hasFlag(flags, "schedule-activity")
        ? getBooleanFlag(flags, "schedule-activity")
        : Boolean(current.schedule_activity ?? false),
      waiting_duration: hasFlag(flags, "waiting-duration")
        ? (getNumberFlag(flags, "waiting-duration") ?? 0)
        : (current.waiting_duration ?? 0),
    };

    const patchResult = await requestJson(config, {
      method: "PATCH",
      path: `/api:qh9OQ3OW/webhook/dev/${id}`,
      body: merged,
      extraHeaders: { Origin: CONSOLE_ORIGIN },
    });

    if (!patchResult.ok) {
      printJson(err(patchResult.error, patchResult.data, false, patchResult.status));
      return;
    }

    if (pretty) {
      const d = patchResult.data;
      process.stderr.write(`\nDev webhook ${id} updated:\n`);
      process.stderr.write(`  URL:               ${d.webhooks}\n`);
      process.stderr.write(`  Status:            ${d.status ? "Active" : "Off"}\n`);
      process.stderr.write(`  Outgoing:          ${d.outgoing ? "Yes" : "No"}\n`);
      process.stderr.write(`  Incoming:          ${d.incoming ? "Yes" : "No"}\n`);
      process.stderr.write(`  Schedule Activity: ${d.schedule_activity ? "Yes" : "No"}\n`);
      process.stderr.write(`  Waiting Duration:  ${d.waiting_duration}s\n`);
      process.stderr.write("\n");
    }

    printJson(ok(patchResult.data));
  } else {
    // IO webhook — id is TEXT type in Xano
    const id = idRaw;

    // Fetch current state to merge
    const listResult = await requestJson(config, {
      method: "GET",
      path: "/api:qh9OQ3OW/user/io/webhook",
    });

    if (!listResult.ok) {
      printJson(
        err(`Failed to fetch current IO webhooks: ${listResult.error}`, listResult.data)
      );
      return;
    }

    const all = Array.isArray(listResult.data) ? listResult.data : [];
    const current = all.find((w) => String(w.id) === String(id));
    if (!current) {
      printJson(
        err(`IO webhook with id "${id}" not found. Use list-webhooks.js --type io to see available.`)
      );
      return;
    }

    // IO PATCH inputs: id (text), webhook (text — singular), status (bool)
    const body = {
      id,
      webhook: hasFlag(flags, "url")
        ? getFlag(flags, "url")
        : (current.webhooks ?? current.webhook),
      status: hasFlag(flags, "status")
        ? getFlag(flags, "status") !== "false"
        : Boolean(current.is_active ?? current.status ?? true),
    };

    const patchResult = await requestJson(config, {
      method: "PATCH",
      path: "/api:qh9OQ3OW/user/io/webhook",
      body,
    });

    if (!patchResult.ok) {
      printJson(err(patchResult.error, patchResult.data, false, patchResult.status));
      return;
    }

    if (pretty) {
      const d = patchResult.data;
      process.stderr.write(`\nIO webhook "${id}" updated:\n`);
      process.stderr.write(`  URL:    ${d.webhooks ?? d.webhook}\n`);
      process.stderr.write(`  Active: ${d.is_active ?? d.status ? "Yes" : "No"}\n`);
      process.stderr.write("\n");
    }

    printJson(ok(patchResult.data));
  }
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/list-broadcasts.js
content: 30 mtime=1775569091.396812813
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/list-broadcasts.js
content: #!/usr/bin/env node
/**
 * list-broadcasts.js — List broadcasts by status (upcoming / previous / ongoing).
 *
 * GET /api:6_ZYypAc/broadcast?require=<upcoming|previous|ongoing>
 *
 * Usage:
 *   node scripts/list-broadcasts.js                          # upcoming (default)
 *   node scripts/list-broadcasts.js --status previous        # completed broadcasts
 *   node scripts/list-broadcasts.js --status ongoing         # currently sending
 *   node scripts/list-broadcasts.js --status upcoming --pretty
 *
 * Flags:
 *   --status <value>   upcoming | previous | ongoing  (default: upcoming)
 *   --pretty           Print human-readable table to stderr  (optional)
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "status": "upcoming",
 *       "broadcasts": [
 *         {
 *           "id": 5,
 *           "broadcast_name": "January Sale",
 *           "template_name": "promo_banner",
 *           "unique_numbers": "1200",
 *           "delivery_mode": "smart",
 *           "delivery_size": "4",
 *           "schedule": 1706184000000,
 *           "broadcast_identifier": "uuid...",
 *           "cost_of_broadcast": 1.44,
 *           "user_selected_read_rate": 95
 *         }
 *       ],
 *       "count": 1
 *     }
 *   }
 *
 * Additional fields on previous/ongoing:
 *   delivery_success, delivery_fail, message_send_count, message_read_count, batch_percentage
 *
 * Notes:
 *   - `schedule` is a Unix millisecond timestamp. Divide by 1000 for seconds.
 *   - `cost_of_broadcast` is in USD.
 *   - All three status variants query the same `broadcast_schedule` Xano table;
 *     Xano filters internally by broadcast status.
 *   - Every broadcast endpoint sends `Origin: https://console.notifyer-systems.com`
 *     because Xano runs a /cors_origin_console check on all /api:6_ZYypAc endpoints.
 *   - Uses console auth mode (Authorization: Bearer <token>).
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const VALID_STATUSES = ["upcoming", "previous", "ongoing"];
const CONSOLE_ORIGIN = "https://console.notifyer-systems.com";

function formatTimestamp(ms) {
  if (!ms) return "N/A";
  return new Date(ms).toLocaleString("en-GB", {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  });
}

function printSummary(broadcasts, status) {
  process.stderr.write(`\nBroadcasts — ${status.toUpperCase()} (${broadcasts.length} total)\n`);
  process.stderr.write(`${"─".repeat(100)}\n`);
  process.stderr.write(
    `${"ID".padEnd(5)} ${"Name".padEnd(22)} ${"Template".padEnd(22)} ${"Numbers".padEnd(9)} ${"Mode".padEnd(10)} ${"Schedule".padEnd(18)} ${"Cost".padEnd(8)}\n`
  );
  process.stderr.write(`${"─".repeat(100)}\n`);

  for (const b of broadcasts) {
    const id = String(b.id ?? "").padEnd(5);
    const name = (b.broadcast_name ?? "").slice(0, 21).padEnd(22);
    const tmpl = (b.template_name ?? "").slice(0, 21).padEnd(22);
    const nums = String(b.unique_numbers ?? "").padEnd(9);
    const mode = (b.delivery_mode ?? "").padEnd(10);
    const sched = formatTimestamp(b.schedule).padEnd(18);
    const cost = `$${(b.cost_of_broadcast ?? 0).toFixed(2)}`.padEnd(8);
    process.stderr.write(`${id} ${name} ${tmpl} ${nums} ${mode} ${sched} ${cost}\n`);
  }
  process.stderr.write("\n");
}

async function main() {
  const flags = parseArgs();
  const statusRaw = getFlag(flags, "status") ?? "upcoming";
  const pretty = getBooleanFlag(flags, "pretty");

  const status = statusRaw.toLowerCase();
  if (!VALID_STATUSES.includes(status)) {
    printJson(
      err(`--status must be one of: ${VALID_STATUSES.join(", ")}. Got: "${statusRaw}"`)
    );
    return;
  }

  const config = loadConfig({ requireToken: true });

  const result = await requestJson(config, {
    method: "GET",
    path: "/api:6_ZYypAc/broadcast",
    query: { require: status },
    extraHeaders: { Origin: CONSOLE_ORIGIN },
  });

  if (!result.ok) {
    printJson(err(result.error, result.data, false, result.status));
    return;
  }

  const broadcasts = Array.isArray(result.data) ? result.data : [];

  if (pretty) printSummary(broadcasts, status);

  printJson(ok({ status, broadcasts, count: broadcasts.length }));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/create-broadcast.js
content: 30 mtime=1775569194.360756953
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/create-broadcast.js
content: #!/usr/bin/env node
/**
 * create-broadcast.js — Create and schedule a WhatsApp broadcast.
 *
 * This script executes the full 3-step broadcast creation workflow:
 *
 *   Step 1 — POST /api:6_ZYypAc/broadcast_test
 *             Initialises a broadcast_schedule record and sends a test WhatsApp
 *             message to verify the template renders correctly.
 *
 *   Step 2 — POST /api:6_ZYypAc/broadcast_user_recipient_numbers  (multipart)
 *             Uploads the recipient CSV file. Xano parses the CSV, deduplicates
 *             phone numbers, calculates cost per country code, and stores the list.
 *
 *   Step 3 — POST /api:6_ZYypAc/broadcast_schedule
 *             Finalises delivery settings (mode, batch size, read rate, schedule
 *             time) and creates the broadcast job.
 *
 * Usage:
 *   node scripts/create-broadcast.js \
 *     --name "January Sale" \
 *     --template-id 42 \
 *     --test-phone "+14155550123" \
 *     --recipients /path/to/recipients.csv \
 *     --schedule "25/01/2025 14:00" \
 *     --delivery-mode smart \
 *     --delivery-size 4 \
 *     --read-rate 95
 *
 *   # With template variables (for templates with {{1}} placeholders):
 *   node scripts/create-broadcast.js \
 *     --name "Order Update" \
 *     --template-id 42 \
 *     --test-phone "+14155550123" \
 *     --variables '{"1":"John","2":"12345"}' \
 *     --recipients /path/to/recipients.csv \
 *     --schedule "25/01/2025 09:00" \
 *     --delivery-mode regular \
 *     --delivery-size 10
 *
 *   # Risk mode (sends without batching — no delivery_size needed):
 *   node scripts/create-broadcast.js \
 *     --name "Urgent Alert" \
 *     --template-id 42 \
 *     --test-phone "+14155550123" \
 *     --recipients /path/to/recipients.csv \
 *     --schedule "25/01/2025 10:00" \
 *     --delivery-mode risk
 *
 * Required flags:
 *   --name <text>              Broadcast display name
 *   --template-id <integer>    Notifyer template ID (from list-templates.js)
 *   --test-phone <text>        Phone number for the test message (include country code, e.g. +1...)
 *   --recipients <path>        Path to the recipient CSV file
 *   --schedule <text>          Schedule datetime: "DD/MM/YYYY HH:mm" (interpreted in the
 *                              server's detected timezone via IP lookup)
 *   --delivery-mode <text>     smart | regular | risk
 *
 * Optional flags:
 *   --variables <json>         Template variable values  e.g. '{"1":"John","2":"#123"}'
 *   --labels <csv>             Comma-separated label names to scope the broadcast
 *   --delivery-size <integer>  Batch size per minute (required for smart/regular; omit for risk)
 *   --read-rate <integer>      Target read rate % (required when --delivery-mode smart)
 *
 * Delivery modes:
 *   smart    — Sends in batches; Xano adjusts pace based on a target read-rate percentage.
 *              Requires --delivery-size and --read-rate.
 *   regular  — Sends in fixed batches per minute. Requires --delivery-size.
 *   risk     — Sends all at once with no batching. Highest delivery risk for Meta ban.
 *              Do NOT use --delivery-size with this mode.
 *
 * CSV format:
 *   The recipient CSV must have a `phone_number` column (and optionally variable
 *   columns like `body1`, `body2`, `button_dynamic_url_value`, `media`).
 *   Download a template CSV using the broadcast console UI after selecting a template.
 *
 *   Example CSV:
 *     phone_number,body1,body2
 *     14155550101,John,12345
 *     14155550102,Jane,67890
 *
 *   Phone numbers in the CSV should NOT include the "+" prefix — Xano handles formatting.
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "broadcast_identifier": "uuid...",
 *       "test": { "success": true, "message": "...", "whatsapp_response_info": { ... } },
 *       "recipients": { "unique_numbers": 1200, "cost_of_broadcast": 1.44, "existing_credit": 5.0 },
 *       "schedule": { "success": true, "message": "...", "broadcast_id": "uuid..." }
 *     }
 *   }
 *
 * Output (step failure):
 *   {
 *     "ok": false,
 *     "error": "Step 1 (test) failed: ...",
 *     "step": 1,
 *     "data": { ... }
 *   }
 *
 * IMPORTANT — Schedule is timezone-sensitive:
 *   Xano resolves the caller's timezone via an IP Address Lookup. The schedule
 *   "25/01/2025 14:00" is interpreted as 2:00 PM in the timezone of the machine
 *   making the API call. Always verify the resulting schedule time in the console.
 *
 * Prerequisites:
 *   - WhatsApp connection must be active (check with get-connection-status.js)
 *   - Template must be approved (check with list-templates.js --status approved)
 *   - Account must have sufficient credit (check with get-user-plan.js)
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { createReadStream, existsSync } from "fs";
import { basename } from "path";
import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const CONSOLE_ORIGIN = "https://console.notifyer-systems.com";
const VALID_MODES = ["smart", "regular", "risk"];

/**
 * Upload recipient CSV via multipart/form-data.
 * Uses raw fetch because requestJson only handles JSON bodies.
 */
async function uploadRecipients(config, broadcastIdentifier, csvPath) {
  const url = `${config.baseUrl}/api:6_ZYypAc/broadcast_user_recipient_numbers`;

  const fileContent = createReadStream(csvPath);
  // Node.js 18+ fetch supports FormData with Blob/File
  const { readFileSync } = await import("fs");
  const fileBytes = readFileSync(csvPath);
  const file = new File([fileBytes], basename(csvPath), { type: "text/csv" });

  const formData = new FormData();
  formData.append("broadcast_identifier", broadcastIdentifier);
  formData.append("file", file);

  let res;
  try {
    res = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${config.token}`,
        Origin: CONSOLE_ORIGIN,
        // Content-Type is NOT set manually — browser/fetch sets it with boundary for FormData
      },
      body: formData,
    });
  } catch (e) {
    return { ok: false, error: `Network error: ${e.message}` };
  }

  let body;
  try {
    body = await res.json();
  } catch {
    body = await res.text().catch(() => null);
  }

  if (!res.ok) {
    const msg =
      (typeof body === "object" && body?.message) ||
      (typeof body === "string" && body) ||
      `HTTP ${res.status}`;
    return { ok: false, error: msg, status: res.status, data: body };
  }

  return { ok: true, data: body };
}

async function main() {
  const flags = parseArgs();

  // --- Required flags ---
  const name = getFlag(flags, "name");
  const templateIdRaw = getFlag(flags, "template-id");
  const testPhone = getFlag(flags, "test-phone");
  const csvPath = getFlag(flags, "recipients");
  const schedule = getFlag(flags, "schedule");
  const deliveryMode = getFlag(flags, "delivery-mode")?.toLowerCase();

  const missing = [];
  if (!name?.trim()) missing.push("--name");
  if (!templateIdRaw) missing.push("--template-id");
  if (!testPhone?.trim()) missing.push("--test-phone");
  if (!csvPath?.trim()) missing.push("--recipients");
  if (!schedule?.trim()) missing.push("--schedule");
  if (!deliveryMode) missing.push("--delivery-mode");

  if (missing.length > 0) {
    printJson(err(`Missing required flags: ${missing.join(", ")}`));
    return;
  }

  const templateId = Number(templateIdRaw);
  if (!Number.isInteger(templateId) || templateId <= 0) {
    printJson(err(`--template-id must be a positive integer, got: ${templateIdRaw}`));
    return;
  }

  if (!VALID_MODES.includes(deliveryMode)) {
    printJson(err(`--delivery-mode must be one of: ${VALID_MODES.join(", ")}. Got: "${deliveryMode}"`));
    return;
  }

  // delivery-size required for smart and regular
  const deliverySizeRaw = getFlag(flags, "delivery-size");
  let deliverySize;
  if (deliveryMode !== "risk") {
    if (!deliverySizeRaw) {
      printJson(
        err(`--delivery-size is required when --delivery-mode is "${deliveryMode}".`)
      );
      return;
    }
    deliverySize = parseInt(deliverySizeRaw, 10);
    if (isNaN(deliverySize) || deliverySize <= 0) {
      printJson(err(`--delivery-size must be a positive integer, got: ${deliverySizeRaw}`));
      return;
    }
  }

  // read-rate required for smart
  const readRateRaw = getFlag(flags, "read-rate");
  let readRate;
  if (deliveryMode === "smart") {
    if (!readRateRaw) {
      printJson(err("--read-rate is required when --delivery-mode is smart."));
      return;
    }
    readRate = parseInt(readRateRaw, 10);
    if (isNaN(readRate) || readRate < 1 || readRate > 100) {
      printJson(err(`--read-rate must be an integer 1–100, got: ${readRateRaw}`));
      return;
    }
  }

  // Validate schedule format: DD/MM/YYYY HH:mm
  if (!/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}$/.test(schedule.trim())) {
    printJson(
      err(
        `--schedule must be in format "DD/MM/YYYY HH:mm" (e.g. "25/01/2025 14:00"). Got: "${schedule}"`
      )
    );
    return;
  }

  // Validate CSV path
  if (!existsSync(csvPath)) {
    printJson(err(`Recipient CSV file not found: ${csvPath}`));
    return;
  }

  // Parse optional flags
  let variables = {};
  const variablesRaw = getFlag(flags, "variables");
  if (variablesRaw) {
    try {
      variables = JSON.parse(variablesRaw);
    } catch {
      printJson(err(`--variables must be valid JSON. Got: ${variablesRaw}`));
      return;
    }
  }

  const labelsRaw = getFlag(flags, "labels");
  const globalLabel = labelsRaw
    ? labelsRaw.split(",").map((l) => l.trim()).filter(Boolean)
    : [];

  const config = loadConfig({ requireToken: true });

  // Generate the broadcast_identifier UUID that links all 3 steps
  const broadcastIdentifier = crypto.randomUUID();

  // ─── Step 1: Send test message ────────────────────────────────────────────
  process.stderr.write(`[1/3] Sending test message to ${testPhone}...\n`);

  const testResult = await requestJson(config, {
    method: "POST",
    path: "/api:6_ZYypAc/broadcast_test",
    body: {
      template_id: templateId,
      phone_number: testPhone.trim(),
      broadcast_identifier: broadcastIdentifier,
      variables,
      global_label: globalLabel,
    },
    extraHeaders: { Origin: CONSOLE_ORIGIN },
  });

  if (!testResult.ok) {
    printJson(
      err(`Step 1 (test) failed: ${testResult.error}`, testResult.data, false, testResult.status)
    );
    return;
  }

  const testData = testResult.data;

  // broadcast_test returns send_message_response via Return statement (not a response key).
  // If `success` is false, the WhatsApp send failed.
  if (testData?.success === false) {
    const whatsappErr = testData?.whatsapp_response_info;
    const msg =
      whatsappErr?.error_data?.details ||
      whatsappErr?.message ||
      testData?.message ||
      "Test message was not delivered by WhatsApp.";
    printJson(err(`Step 1 (test) failed: ${msg}`, testData, false));
    return;
  }

  process.stderr.write(`    ✓ Test message sent. broadcast_identifier: ${broadcastIdentifier}\n`);

  // ─── Step 2: Upload recipient CSV ─────────────────────────────────────────
  process.stderr.write(`[2/3] Uploading recipients from ${basename(csvPath)}...\n`);

  const uploadResult = await uploadRecipients(config, broadcastIdentifier, csvPath);

  if (!uploadResult.ok) {
    printJson(
      err(
        `Step 2 (upload) failed: ${uploadResult.error}`,
        uploadResult.data,
        false,
        uploadResult.status
      )
    );
    return;
  }

  const recipientsData = uploadResult.data;
  const uniqueNumbers = recipientsData?.unique_numbers ?? 0;
  const costOfBroadcast = recipientsData?.cost_of_broadcast ?? 0;

  process.stderr.write(
    `    ✓ ${uniqueNumbers} unique numbers loaded. Estimated cost: $${costOfBroadcast.toFixed(4)}\n`
  );

  // ─── Step 3: Schedule the broadcast ───────────────────────────────────────
  process.stderr.write(`[3/3] Scheduling broadcast for ${schedule.trim()}...\n`);

  const schedulePayload = {
    broadcast_identifier: broadcastIdentifier,
    schedule: schedule.trim(),
    broadcast_name: name.trim(),
    delivery_mode: deliveryMode,
  };

  if (deliveryMode !== "risk") {
    schedulePayload.delivery_size = deliverySize;
  }
  if (deliveryMode === "smart") {
    schedulePayload.read_rate = readRate;
  }

  const scheduleResult = await requestJson(config, {
    method: "POST",
    path: "/api:6_ZYypAc/broadcast_schedule",
    body: schedulePayload,
    extraHeaders: { Origin: CONSOLE_ORIGIN },
  });

  if (!scheduleResult.ok) {
    printJson(
      err(
        `Step 3 (schedule) failed: ${scheduleResult.error}`,
        scheduleResult.data,
        false,
        scheduleResult.status
      )
    );
    return;
  }

  const scheduleData = scheduleResult.data;
  process.stderr.write(`    ✓ Broadcast scheduled.\n\n`);

  printJson(
    ok({
      broadcast_identifier: broadcastIdentifier,
      test: testData,
      recipients: recipientsData,
      schedule: scheduleData,
    })
  );
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/create-template.js
content: 30 mtime=1775564130.332351827
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/create-template.js
content: #!/usr/bin/env node
/**
 * create-template.js — Submit a WhatsApp message template for Meta review.
 *
 * POST /api:AFRA_QCy/create
 * Media pre-upload: GET /api:ox_LN9zX/get_file_base46_encode?attachment=<url>
 *
 * Usage (text template):
 *   node scripts/create-template.js \
 *     --name order_confirmation \
 *     --category MARKETING \
 *     --body "Hello {{1}}, your order #{{2}} has been confirmed."
 *
 * Usage (with variable example values):
 *   node scripts/create-template.js \
 *     --name order_confirmation \
 *     --category MARKETING \
 *     --body "Hello {{1}}, your order #{{2}} has been confirmed." \
 *     --variables '{"1":"John","2":"12345"}'
 *
 * Usage (with image attachment):
 *   node scripts/create-template.js \
 *     --name promo_banner \
 *     --category MARKETING \
 *     --body "Check out our latest offer!" \
 *     --type image \
 *     --media-url "https://example.com/banner.jpg"
 *
 * Usage (with pre-uploaded media handle):
 *   node scripts/create-template.js \
 *     --name promo_doc \
 *     --category UTILITY \
 *     --body "Here is your invoice." \
 *     --type document \
 *     --media-handle "<handle from upload>"
 *
 * Usage (with buttons):
 *   node scripts/create-template.js \
 *     --name order_confirmation \
 *     --category MARKETING \
 *     --body "Your order {{1}} is ready!" \
 *     --variables '{"1":"12345"}' \
 *     --buttons '[{"type":"Quick Reply","text":"Track Order"},{"type":"Quick Reply","text":"Cancel"}]'
 *
 * Usage (AUTHENTICATION template):
 *   node scripts/create-template.js \
 *     --name verify_login \
 *     --category AUTHENTICATION \
 *     --language en \
 *     --expiry 10 \
 *     --otp-button-text "Copy Code"
 *
 * Flags:
 *   --name <name>           Template name: lowercase, underscores, no leading digit  (required)
 *   --category <value>      MARKETING | UTILITY | AUTHENTICATION                    (required)
 *   --body <text>           Message body text; use {{1}}, {{2}} for variables        (required unless AUTHENTICATION)
 *   --language <code>       Language code, e.g. en, es, fr (default: en)            (optional)
 *   --type <value>          text | image | document | video (default: text)          (optional)
 *   --media-url <url>       Public URL of media to pre-upload (for non-text types)   (optional)
 *   --media-handle <handle> Pre-uploaded media handle (alternative to --media-url)   (optional)
 *   --variables <json>      JSON object mapping variable index to example value      (optional)
 *                           e.g. '{"1":"John","2":"12345"}'
 *   --buttons <json>        JSON array of button objects (see Button shapes below)   (optional)
 *   --expiry <minutes>      AUTHENTICATION only: code expiry in minutes (default 10) (optional)
 *   --otp-button-text <txt> AUTHENTICATION only: button label (default "Copy Code")  (optional)
 *   --no-security-rec       AUTHENTICATION only: omit security recommendation footer (optional)
 *
 * Button shapes (pass as --buttons JSON array):
 *   Quick Reply:       { "type": "Quick Reply", "text": "Yes" }
 *   Visit Website:     { "type": "Visit Website", "buttonText": "View", "urlType": "static",
 *                        "buttonUrl": "https://example.com" }
 *   Visit Website (dynamic URL):
 *                      { "type": "Visit Website", "buttonText": "Track", "urlType": "dynamic",
 *                        "buttonUrl": "https://example.com/track/", "dynamicValue": "order_id" }
 *   Call Phone Number: { "type": "Call Phone Number", "buttonText": "Call Us",
 *                        "phoneNumber": "+14155550123" }
 *   Copy Offer Code:   { "type": "Copy Offer Code", "code": "SAVE20" }
 *
 * Button limits:
 *   Quick Reply: up to 10 | Visit Website: up to 2 | Call Phone Number: 1 | Copy Offer Code: 1
 *   Total: up to 10 buttons
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": { ...Meta API response from WhatsApp Business API }
 *   }
 *
 *   The response is the raw Meta API response returned by WhatsApp's template
 *   creation endpoint (var:request_hit_into_whatsapp in Xano). It typically contains
 *   the template ID, status ("PENDING"), and category assigned by Meta. The template
 *   is stored in Notifyer's database only if Meta accepted it.
 *
 * Output (duplicate name):
 *   { "ok": false, "error": "A template named 'order_confirm' already exists.", "blocked": true }
 *
 * Output (error):
 *   { "ok": false, "error": "...", "data": {...} }
 *
 * Notes:
 *   - Template names must be unique per workspace. Xano checks for an existing template
 *     with the same name (step 2 of the function stack) and fires a Precondition error
 *     (HTTP 400) if a duplicate is found. The script surfaces this as a friendly error.
 *   - Template names: lowercase letters, digits, underscores only; cannot start with a digit.
 *   - Meta reviews all templates. Approval typically takes under 60 seconds.
 *   - A newly submitted template starts with status "PENDING". Poll with get-template.js
 *     to check when status changes to "approved" or "rejected".
 *   - The template is only stored in Notifyer's database if Meta accepts it. If the Meta
 *     API call fails, the template will NOT appear in list-templates.js output.
 *   - For AUTHENTICATION templates: the body is auto-generated by Xano; --body is not needed.
 *   - Body variables ({{1}}, {{2}}) must all have example values in --variables.
 *     Meta requires sample values during the review process.
 *   - For non-text types, one of --media-url or --media-handle is required.
 *     --media-url triggers an automatic pre-upload call to get a handle.
 *   - Supported media formats: PNG, JPG (image), MP4 (video), PDF (document).
 *   - Uses console auth mode (Authorization: Bearer <token>).
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const VALID_CATEGORIES = ["MARKETING", "UTILITY", "AUTHENTICATION"];
const VALID_TYPES = ["text", "image", "document", "video"];

const BUTTON_LIMITS = {
  "Quick Reply": 10,
  "Visit Website": 2,
  "Call Phone Number": 1,
  "Copy Offer Code": 1,
};
const TOTAL_BUTTON_LIMIT = 10;

function usage() {
  console.error(`
Usage:
  node scripts/create-template.js --name <name> --category <cat> --body "<text>" [options]

Required:
  --name <name>           Lowercase, underscores, no leading digit (e.g. order_confirm)
  --category <value>      MARKETING | UTILITY | AUTHENTICATION
  --body <text>           Message body (use {{1}}, {{2}} for variables;
                          not needed for AUTHENTICATION)

Options:
  --language <code>       Language code (default: en)
  --type <value>          text | image | document | video (default: text)
  --media-url <url>       Public URL of media to pre-upload (required for non-text)
  --media-handle <handle> Pre-uploaded media handle (alternative to --media-url)
  --variables <json>      JSON mapping of variable index -> example value
                          e.g. '{"1":"John","2":"12345"}'
  --buttons <json>        JSON array of button objects (see SKILL.md for shapes)
  --expiry <minutes>      AUTHENTICATION only: code expiry minutes (default: 10)
  --otp-button-text <txt> AUTHENTICATION only: button label (default: "Copy Code")
  --no-security-rec       AUTHENTICATION only: omit security recommendation

Examples:
  node scripts/create-template.js --name promo --category MARKETING --body "Sale {{1}}!" --variables '{"1":"50%"}'
  node scripts/create-template.js --name invoice --category UTILITY --body "Invoice attached." --type document --media-url "https://example.com/invoice.pdf"
  node scripts/create-template.js --name verify_otp --category AUTHENTICATION
`);
  process.exit(1);
}

/**
 * Validate and normalise a template name.
 * Rules (from frontend): lowercase, no leading digit, only a-z 0-9 _ and unicode letters.
 */
function validateName(name) {
  if (!name || !name.trim()) return { valid: false, reason: "name is empty" };
  if (/^\d/.test(name)) return { valid: false, reason: "name cannot start with a digit" };
  if (/[A-Z]/.test(name)) return { valid: false, reason: "name must be lowercase (use underscores instead of spaces)" };
  if (/\s/.test(name)) return { valid: false, reason: "name cannot contain spaces (use underscores)" };
  if (name.length > 512) return { valid: false, reason: "name exceeds 512 characters" };
  return { valid: true };
}

/**
 * Extract variable indices from body text: {{1}}, {{2}}, etc.
 * Returns a sorted, unique array of ints.
 */
function extractBodyVariables(body) {
  const matched = body.match(/{{\d+}}/g) ?? [];
  const unique = [...new Set(matched.map((v) => parseInt(v.replace(/{{|}}/g, ""), 10)))];
  return unique.sort((a, b) => a - b);
}

/**
 * Validate buttons array and assign sequential IDs.
 */
function prepareButtons(buttons) {
  if (!Array.isArray(buttons)) {
    return { ok: false, error: "--buttons must be a JSON array" };
  }

  if (buttons.length > TOTAL_BUTTON_LIMIT) {
    return { ok: false, error: `Total button limit is ${TOTAL_BUTTON_LIMIT}` };
  }

  const typeCounts = {};
  const prepared = [];

  for (let i = 0; i < buttons.length; i++) {
    const btn = buttons[i];
    const id = i + 1;

    if (!btn.type) {
      return { ok: false, error: `Button at index ${i} is missing "type"` };
    }

    const limit = BUTTON_LIMITS[btn.type];
    if (limit === undefined) {
      return {
        ok: false,
        error: `Unknown button type "${btn.type}". Valid types: ${Object.keys(BUTTON_LIMITS).join(", ")}`,
      };
    }

    typeCounts[btn.type] = (typeCounts[btn.type] ?? 0) + 1;
    if (typeCounts[btn.type] > limit) {
      return {
        ok: false,
        error: `Too many "${btn.type}" buttons (limit: ${limit})`,
      };
    }

    switch (btn.type) {
      case "Quick Reply":
        if (!btn.text?.trim()) {
          return { ok: false, error: `Quick Reply button at index ${i} is missing "text"` };
        }
        prepared.push({ id, type: btn.type, text: btn.text });
        break;

      case "Visit Website":
        if (!btn.buttonText?.trim()) {
          return { ok: false, error: `Visit Website button at index ${i} is missing "buttonText"` };
        }
        if (!btn.urlType || !["static", "dynamic"].includes(btn.urlType)) {
          return { ok: false, error: `Visit Website button at index ${i}: "urlType" must be "static" or "dynamic"` };
        }
        if (btn.urlType === "dynamic" && !btn.dynamicValue?.trim()) {
          return { ok: false, error: `Visit Website button at index ${i}: "dynamicValue" is required for dynamic URL` };
        }
        if (btn.urlType === "static" && !btn.buttonUrl?.trim()) {
          return { ok: false, error: `Visit Website button at index ${i}: "buttonUrl" is required for static URL` };
        }
        prepared.push({
          id,
          type: btn.type,
          buttonText: btn.buttonText,
          urlType: btn.urlType,
          buttonUrl: btn.buttonUrl ?? "",
          dynamicValue: btn.dynamicValue ?? "",
        });
        break;

      case "Call Phone Number":
        if (!btn.buttonText?.trim()) {
          return { ok: false, error: `Call Phone Number button at index ${i} is missing "buttonText"` };
        }
        if (!btn.phoneNumber?.trim()) {
          return { ok: false, error: `Call Phone Number button at index ${i} is missing "phoneNumber"` };
        }
        prepared.push({
          id,
          type: btn.type,
          buttonText: btn.buttonText,
          phoneNumber: btn.phoneNumber,
        });
        break;

      case "Copy Offer Code":
        if (!btn.code?.trim()) {
          return { ok: false, error: `Copy Offer Code button at index ${i} is missing "code"` };
        }
        prepared.push({ id, type: btn.type, code: btn.code });
        break;

      default:
        return { ok: false, error: `Unknown button type "${btn.type}"` };
    }
  }

  return { ok: true, data: prepared };
}

/**
 * Pre-upload media from a public URL and return the handle string.
 * GET /api:ox_LN9zX/get_file_base46_encode?attachment=<url>
 */
async function uploadMedia(config, mediaUrl) {
  process.stderr.write(`  Uploading media: ${mediaUrl}\n`);
  const result = await requestJson(config, {
    method: "GET",
    path: "/api:ox_LN9zX/get_file_base46_encode",
    query: { attachment: mediaUrl },
  });

  if (!result.ok) {
    return { ok: false, error: `Media upload failed: ${result.error}` };
  }

  if (!result.data?.success || !result.data?.handle) {
    return {
      ok: false,
      error: `Media upload failed: ${result.data?.error ?? "no handle returned"}`,
    };
  }

  process.stderr.write(`  Media handle: ${result.data.handle}\n`);
  return { ok: true, handle: result.data.handle };
}

async function main() {
  const flags = parseArgs();
  const name = getFlag(flags, "name");
  const category = getFlag(flags, "category")?.toUpperCase();
  const body = getFlag(flags, "body") ?? "";
  const language = getFlag(flags, "language") ?? "en";
  const templateType = getFlag(flags, "type") ?? "text";
  const mediaUrl = getFlag(flags, "media-url");
  const mediaHandle = getFlag(flags, "media-handle");
  const variablesRaw = getFlag(flags, "variables");
  const buttonsRaw = getFlag(flags, "buttons");
  const expiry = getFlag(flags, "expiry") ?? "10";
  const otpButtonText = getFlag(flags, "otp-button-text") ?? "Copy Code";
  const noSecurityRec = getBooleanFlag(flags, "no-security-rec");

  // ── Validate required fields ────────────────────────────────────────────────
  if (!name) {
    console.error("Error: --name is required.\n");
    usage();
  }

  const nameCheck = validateName(name);
  if (!nameCheck.valid) {
    printJson(err(`Invalid template name: ${nameCheck.reason}`));
  }

  if (!category) {
    console.error("Error: --category is required.\n");
    usage();
  }

  if (!VALID_CATEGORIES.includes(category)) {
    printJson(
      err(
        `Invalid --category "${category}". Must be one of: ${VALID_CATEGORIES.join(", ")}`
      )
    );
  }

  if (!VALID_TYPES.includes(templateType)) {
    printJson(
      err(
        `Invalid --type "${templateType}". Must be one of: ${VALID_TYPES.join(", ")}`
      )
    );
  }

  // Body is required for non-AUTHENTICATION templates
  if (category !== "AUTHENTICATION" && !body.trim()) {
    console.error("Error: --body is required for MARKETING and UTILITY templates.\n");
    usage();
  }

  // ── Parse variables ─────────────────────────────────────────────────────────
  let variableValues = {};
  if (variablesRaw) {
    try {
      variableValues = JSON.parse(variablesRaw);
    } catch {
      printJson(err('--variables must be valid JSON, e.g. \'{"1":"John","2":"12345"}\''));
    }
  }

  // Auto-detect body variables and verify all have example values
  const bodyVariables = category !== "AUTHENTICATION" ? extractBodyVariables(body) : [];
  const missingExamples = bodyVariables.filter(
    (v) => !variableValues[String(v)] && !variableValues[v]
  );
  if (missingExamples.length > 0) {
    printJson(
      err(
        `Missing example values for body variables: ${missingExamples.map((v) => `{{${v}}}`).join(", ")}. ` +
          `Provide them with --variables '{"${missingExamples[0]}":"example value",...}'`
      )
    );
  }

  // Normalise variableValues to use numeric keys (Xano convention)
  const normalisedVariableValues = {};
  for (const [k, v] of Object.entries(variableValues)) {
    normalisedVariableValues[parseInt(k, 10)] = v;
  }

  // ── Parse buttons ────────────────────────────────────────────────────────────
  let preparedButtons = [];
  if (buttonsRaw) {
    let rawButtons;
    try {
      rawButtons = JSON.parse(buttonsRaw);
    } catch {
      printJson(err("--buttons must be a valid JSON array"));
    }
    const btnResult = prepareButtons(rawButtons);
    if (!btnResult.ok) {
      printJson(err(btnResult.error));
    }
    preparedButtons = btnResult.data;
  }

  // ── Handle media upload ──────────────────────────────────────────────────────
  const config = loadConfig({ requireToken: true });
  let finalMediaHandle = "";

  if (templateType !== "text") {
    if (mediaHandle) {
      finalMediaHandle = mediaHandle;
    } else if (mediaUrl) {
      const uploadResult = await uploadMedia(config, mediaUrl);
      if (!uploadResult.ok) {
        printJson(err(uploadResult.error));
      }
      finalMediaHandle = uploadResult.handle;
    } else {
      printJson(
        err(
          `--media-url or --media-handle is required for --type "${templateType}". ` +
            "Provide a public media URL (PNG/JPG/MP4/PDF) via --media-url."
        )
      );
    }
  }

  // ── Build payload ────────────────────────────────────────────────────────────
  const payload = {
    name,
    category,
    language,
    templateType,
    mediaUrl: finalMediaHandle,
    temBody: body,
    mediaUrlVariableValues: "",
    bodyVariables,
    variableValues: normalisedVariableValues,
    buttons: preparedButtons,
    ...(category === "AUTHENTICATION" && {
      addSecurityRecommendation: !noSecurityRec,
      codeExpirationMinutes: parseInt(expiry, 10),
      otpButtonText,
    }),
  };

  // ── Submit ───────────────────────────────────────────────────────────────────
  const result = await requestJson(config, {
    method: "POST",
    path: "/api:AFRA_QCy/create",
    body: payload,
  });

  if (!result.ok) {
    // Step 6 of the Xano function stack is a Precondition that fires if a template
    // with the same name already exists (var:templates != false). Xano returns HTTP 400
    // with a "Precondition Failed" message in that case.
    const isDuplicate =
      result.status === 400 &&
      typeof result.error === "string" &&
      result.error.toLowerCase().includes("precondition");

    if (isDuplicate) {
      printJson(
        err(
          `A template named '${name}' already exists.`,
          result.data,
          true,
          result.status
        )
      );
    }

    printJson(err(result.error, result.data, false, result.status));
  }

  // The response is the raw Meta API response (var:request_hit_into_whatsapp).
  // It contains what WhatsApp's Business API returned for the template creation request.
  // The template is stored in Notifyer only if Meta accepted it.
  printJson(ok(result.data));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/delete-webhook.js
content: 30 mtime=1775713737.827647613
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/delete-webhook.js
content: #!/usr/bin/env node
/**
 * delete-webhook.js — Permanently delete a webhook endpoint.
 *
 * DELETE /api:qh9OQ3OW/webhook/dev/{id}         (--type dev, default)
 * DELETE /api:qh9OQ3OW/user/io/webhook?id=<id>  (--type io, id is text/string)
 *
 * Usage:
 *   node scripts/delete-webhook.js --id 5 --confirm
 *   node scripts/delete-webhook.js --type io --id "abc123" --confirm
 *
 * Required Flags:
 *   --id <id>      Webhook ID to delete. Dev: integer. IO: string/text.
 *   --confirm      Safety gate — must be passed to execute deletion.
 *
 * Optional Flags:
 *   --type dev|io  Webhook type (default: dev)
 *   --pretty       Print summary to stderr
 *
 * Output (dev webhook success):
 *   { "ok": true, "data": { "deleted": true, "id": 5, "type": "dev" } }
 *
 *   NOTE: DELETE /webhook/dev/:id returns an empty body (no response keys in Xano).
 *   The script synthesizes the response so agents can confirm success.
 *
 * Output (IO webhook success):
 *   { "ok": true, "data": { "success": "true", "id": "abc123", "type": "io" } }
 *
 *   NOTE: Xano returns { "success": "true" } — success is a STRING "true", not
 *   a boolean. This is documented Xano behavior; the script passes it through
 *   and adds id and type fields for agent clarity.
 *
 * CRITICAL — Dev Webhook Security:
 *   DELETE /webhook/dev/:id is a PUBLIC ENDPOINT in Xano.
 *   Xano only runs /cors_origin_console (CORS check) — there is NO /get_user call.
 *   This script mitigates this at the script level with an ownership check:
 *     1. GET /webhook/dev (fully authenticated — runs /get_user) to list all
 *        webhooks belonging to the authenticated account.
 *     2. Confirm the requested ID appears in that list before deleting.
 *   This means an attacker with only a webhook ID but no valid token cannot
 *   use this script to delete a webhook they don't own.
 *   Note: the raw API endpoint remains public — this is a script-level defence.
 *
 * IO vs Dev difference:
 *   - IO DELETE is fully authenticated (Xano runs /get_user) — standard security.
 *   - IO id is TEXT type (not integer). Pass as a string.
 *   - IO CORS: not required.
 *   - IO response: { success: "true" } (as JSON body from Xano Return step).
 *
 * CORS:
 *   Dev webhook endpoints run /cors_origin_console. The script sends
 *   Origin: https://console.notifyer-systems.com automatically.
 *   IO webhook endpoints do NOT require a CORS header.
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const CONSOLE_ORIGIN = "https://console.notifyer-systems.com";
const VALID_TYPES = ["dev", "io"];

async function main() {
  const flags = parseArgs();
  const type = (getFlag(flags, "type") ?? "dev").toLowerCase();
  const pretty = getBooleanFlag(flags, "pretty");
  const idRaw = getFlag(flags, "id");
  const confirmed = getBooleanFlag(flags, "confirm");

  if (!VALID_TYPES.includes(type)) {
    printJson(err(`--type must be "dev" or "io". Got: "${type}"`));
    return;
  }

  if (!idRaw) {
    printJson(err("--id is required."));
    return;
  }

  if (!confirmed) {
    printJson(
      err(
        "Add --confirm to proceed. This action permanently deletes the webhook and cannot be undone."
      )
    );
    return;
  }

  const config = loadConfig({ requireToken: true });

  if (type === "dev") {
    const id = parseInt(idRaw, 10);
    if (isNaN(id)) {
      printJson(err("--id must be an integer for dev webhooks."));
      return;
    }

    // Ownership check: GET /webhook/dev is fully authenticated (runs /get_user).
    // Verify the requested ID belongs to this account before calling DELETE,
    // which has no server-side user auth check in Xano.
    const listResult = await requestJson(config, {
      method: "GET",
      path: "/api:qh9OQ3OW/webhook/dev",
      extraHeaders: { Origin: CONSOLE_ORIGIN },
    });

    if (!listResult.ok) {
      printJson(err(`Ownership check failed: could not list webhooks. ${listResult.error}`, listResult.data, false, listResult.status));
      return;
    }

    const webhooks = Array.isArray(listResult.data) ? listResult.data : (listResult.data?.webhooks ?? []);
    const owns = webhooks.some((w) => Number(w.id) === id);
    if (!owns) {
      printJson(err(
        `Dev webhook ID ${id} was not found in your account's webhook list. ` +
        "Deletion blocked — only webhooks belonging to the authenticated account can be deleted.",
        null,
        true
      ));
      return;
    }

    const result = await requestJson(config, {
      method: "DELETE",
      path: `/api:qh9OQ3OW/webhook/dev/${id}`,
      extraHeaders: { Origin: CONSOLE_ORIGIN },
    });

    // Xano returns empty body (no Response keys defined) → result.data may be null/{}
    // Treat HTTP 2xx as success regardless of body.
    if (!result.ok) {
      printJson(err(result.error, result.data, false, result.status));
      return;
    }

    if (pretty) {
      process.stderr.write(`\nDev webhook ${id} deleted.\n\n`);
    }

    printJson(ok({ deleted: true, id, type: "dev" }));
  } else {
    // IO webhook — id is TEXT type in Xano
    const id = idRaw;

    // IO PATCH sends id as query param or body? From Xano: input "id" is text.
    // The frontend sends PATCH /user/io/webhook with body { id, webhook, status }
    // For DELETE /user/io/webhook: Xano input is "id" (text).
    // Looking at Xano DELETE screenshot: inputs show "id" as text field.
    // The function sends id as a query parameter based on Xano pattern.
    const result = await requestJson(config, {
      method: "DELETE",
      path: `/api:qh9OQ3OW/user/io/webhook?id=${encodeURIComponent(id)}`,
    });

    if (!result.ok) {
      printJson(err(result.error, result.data, false, result.status));
      return;
    }

    // Xano explicitly returns { "success": "true" } (string, not boolean)
    // via Return step: json_decode('{"success":"true"}')
    if (pretty) {
      process.stderr.write(`\nIO webhook "${id}" deleted.\n\n`);
    }

    printJson(
      ok({
        ...(result.data ?? {}),
        id,
        type: "io",
      })
    );
  }
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/create-webhook.js
content: 30 mtime=1775664960.561749843
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/create-webhook.js
content: #!/usr/bin/env node
/**
 * create-webhook.js — Create a new webhook endpoint.
 *
 * POST /api:qh9OQ3OW/webhook/dev/create    (--type dev, default)
 * POST /api:qh9OQ3OW/user/io/webhook       (--type io)
 *
 * Usage:
 *   # Dev webhook (for n8n, Make, Zapier integrations):
 *   node scripts/create-webhook.js --url "https://hook.eu2.make.com/abc123"
 *   node scripts/create-webhook.js --url "https://..." --no-outgoing --no-incoming
 *   node scripts/create-webhook.js --url "https://..." --schedule-activity --waiting-duration 3600
 *   node scripts/create-webhook.js --url "https://..." --signature
 *
 *   # IO webhook (bidirectional incoming & outgoing):
 *   node scripts/create-webhook.js --type io --url "https://myapp.com/webhook"
 *   node scripts/create-webhook.js --type io --url "https://myapp.com/webhook" --signature
 *
 * Required Flags:
 *   --url <url>                  The HTTPS endpoint URL to receive webhook events
 *
 * Dev-specific Flags (ignored for --type io):
 *   --outgoing / --no-outgoing   Include outgoing messages in triggers (default: true)
 *   --incoming / --no-incoming   Include incoming messages in triggers (default: true)
 *   --schedule-activity          Enable schedule-based activity trigger (default: false)
 *   --waiting-duration <secs>    Wait time in seconds before triggering (required when --schedule-activity)
 *   --no-status                  Create webhook as inactive (default: active/true)
 *
 * Shared Flags:
 *   --signature                  Generate an HMAC signature secret key (default: false)
 *   --type dev|io                Webhook type (default: dev)
 *   --pretty                     Print summary to stderr
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "id": 5,
 *       "webhooks": "https://hook.eu2.make.com/abc123",
 *       "status": true,
 *       "outgoing": true,
 *       "incoming": true,
 *       "schedule_activity": false,
 *       "waiting_duration": 0,
 *       "signature_secret": null,
 *       "created_at": 1706184000000
 *     }
 *   }
 *
 * Preconditions:
 *   - Dev webhook: a webhook with the same URL must not already exist.
 *     Returns { ok: false, blocked: true } if duplicate URL is detected (HTTP 400).
 *   - Dev webhook: active WhatsApp connection recommended for events to fire.
 *   - IO webhook: no explicit duplicate check observed in Xano.
 *
 * Notes on --signature:
 *   When --signature is passed (active_signature: true), Xano's /cors_origin_console
 *   or /get_user function generates a secret key via "Create Secret Key" and stores it
 *   in signature_secret. The response will include the signature_secret value.
 *   Keep this secret; it is used to verify HMAC signatures on incoming webhook events.
 *
 * Waiting Duration:
 *   waiting_duration is an integer number of seconds. The Xano input type is `integer`.
 *   Example: --waiting-duration 3600 = 1 hour.
 *
 * CORS:
 *   Dev webhook endpoints run /cors_origin_console. The script sends
 *   Origin: https://console.notifyer-systems.com automatically.
 *   IO webhook endpoints do NOT require a CORS header.
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag, getNumberFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const CONSOLE_ORIGIN = "https://console.notifyer-systems.com";
const VALID_TYPES = ["dev", "io"];

function boolFlagWithDefault(flags, name, defaultValue) {
  const v = flags[name];
  if (v === undefined) return defaultValue;
  if (typeof v === "boolean") return v;
  return v !== "false" && v !== "0";
}

async function main() {
  const flags = parseArgs();
  const type = (getFlag(flags, "type") ?? "dev").toLowerCase();
  const pretty = getBooleanFlag(flags, "pretty");
  const url = getFlag(flags, "url");

  if (!VALID_TYPES.includes(type)) {
    printJson(err(`--type must be "dev" or "io". Got: "${type}"`));
    return;
  }

  if (!url) {
    printJson(err("--url is required. Provide the target webhook endpoint URL."));
    return;
  }

  if (!url.startsWith("http")) {
    printJson(err("--url must be a valid HTTP(S) URL."));
    return;
  }

  const config = loadConfig({ requireToken: true });

  if (type === "dev") {
    const outgoing = boolFlagWithDefault(flags, "outgoing", true);
    const incoming = boolFlagWithDefault(flags, "incoming", true);
    const scheduleActivity = getBooleanFlag(flags, "schedule-activity");
    const waitingDuration = getNumberFlag(flags, "waiting-duration") ?? 0;
    const signature = getBooleanFlag(flags, "signature");
    const status = boolFlagWithDefault(flags, "status", true);

    if (scheduleActivity && waitingDuration === 0) {
      process.stderr.write(
        "Warning: --schedule-activity is set but --waiting-duration is 0 (or not provided). " +
          "Consider passing --waiting-duration <seconds>.\n"
      );
    }

    const body = {
      webhooks: url,
      status,
      outgoing,
      incoming,
      schedule_activity: scheduleActivity,
      waiting_duration: waitingDuration,
      active_signature: signature,
    };

    const result = await requestJson(config, {
      method: "POST",
      path: "/api:qh9OQ3OW/webhook/dev/create",
      body,
      extraHeaders: { Origin: CONSOLE_ORIGIN },
    });

    if (!result.ok) {
      if (result.status === 400) {
        printJson(
          err(
            `A webhook pointing to "${url}" already exists. Use list-webhooks.js to view existing webhooks.`,
            result.data,
            true
          )
        );
      } else {
        printJson(err(result.error, result.data, false, result.status));
      }
      return;
    }

    if (pretty) {
      const d = result.data;
      process.stderr.write("\nDev webhook created:\n");
      process.stderr.write(`  ID:                ${d.id}\n`);
      process.stderr.write(`  URL:               ${d.webhooks}\n`);
      process.stderr.write(`  Status:            ${d.status ? "Active" : "Off"}\n`);
      process.stderr.write(`  Outgoing:          ${d.outgoing ? "Yes" : "No"}\n`);
      process.stderr.write(`  Incoming:          ${d.incoming ? "Yes" : "No"}\n`);
      process.stderr.write(`  Schedule Activity: ${d.schedule_activity ? "Yes" : "No"}\n`);
      process.stderr.write(`  Waiting Duration:  ${d.waiting_duration}s\n`);
      if (d.signature_secret) {
        process.stderr.write(`  Signature [REDACTED_SECRET]  ← SAVE THIS!\n`);
      }
      process.stderr.write("\n");
    }

    printJson(ok(result.data));
  } else {
    // IO webhook
    const signature = getBooleanFlag(flags, "signature");
    const status = boolFlagWithDefault(flags, "status", true);

    const body = {
      webhook: url,
      status,
      active_signature: signature,
    };

    const result = await requestJson(config, {
      method: "POST",
      path: "/api:qh9OQ3OW/user/io/webhook",
      body,
    });

    if (!result.ok) {
      printJson(err(result.error, result.data, false, result.status));
      return;
    }

    if (pretty) {
      const d = result.data;
      process.stderr.write("\nIO webhook created:\n");
      process.stderr.write(`  ID:               ${d.id}\n`);
      process.stderr.write(`  URL:              ${d.webhooks ?? d.webhook}\n`);
      process.stderr.write(`  Active:           ${d.is_active ?? d.status ? "Yes" : "No"}\n`);
      if (d.signature_secret) {
        process.stderr.write(`  Signature [REDACTED_SECRET]  ← SAVE THIS!\n`);
      }
      process.stderr.write("\n");
    }

    printJson(ok(result.data));
  }
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/get-message-analytics.js
content: 30 mtime=1775571877.194951949
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/get-message-analytics.js
content: #!/usr/bin/env node
/**
 * get-message-analytics.js — Retrieve messaging analytics summary for a date range.
 *
 * GET /api:5l-RgW1B/anslytics?start_timestamp=<ms>&end_timestamp=<ms>
 *
 * Usage:
 *   node scripts/get-message-analytics.js                        # last 7 days (default)
 *   node scripts/get-message-analytics.js --days 30              # last 30 days
 *   node scripts/get-message-analytics.js --from 2025-01-01 --to 2025-01-31
 *   node scripts/get-message-analytics.js --days 7 --pretty
 *
 * Flags:
 *   --days <n>          Last N days shortcut (default: 7). Ignored if --from/--to given.
 *   --from YYYY-MM-DD   Start date (inclusive, start of day local time)
 *   --to   YYYY-MM-DD   End date   (inclusive, end of day local time)
 *   --pretty            Print human-readable summary to stderr
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "total_sent": 1200,
 *       "sent_count": 1180,
 *       "delivered_count": 1050,
 *       "read_count": 890,
 *       "read_rate": "74.2%",
 *       "delivery_rate": "87.5%",
 *       "start_readable_time": "2025-01-01T00:00:00.000Z",
 *       "end_readable_time": "2025-01-31T23:59:59.999Z",
 *       "period": { "from_ms": 1735689600000, "to_ms": 1738367999999 }
 *     }
 *   }
 *
 * Notes:
 *   - The Xano endpoint path is "anslytics" (with a typo) — this is the real path.
 *   - start_timestamp / end_timestamp are passed as Unix milliseconds (text strings).
 *   - Xano queries the `conversation` table filtered by timestamp range.
 *   - `total_sent`   = all messages attempted in the period.
 *   - `sent_count`   = messages with a confirmed "sent" status from Meta.
 *   - `delivered_count` = messages confirmed delivered to the device.
 *   - `read_count`   = messages opened by the recipient.
 *   - `read_rate` and `delivery_rate` are calculated by this script (not Xano).
 *   - Xano also returns `start_readable_time` and `end_readable_time` as formatted strings.
 *   - Uses console auth mode (Authorization: Bearer <token>). No CORS header needed.
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

function parseDateToMs(dateStr, endOfDay = false) {
  const d = new Date(dateStr + "T00:00:00");
  if (isNaN(d.getTime())) return null;
  if (endOfDay) d.setHours(23, 59, 59, 999);
  return d.getTime();
}

function getLastNDays(n) {
  const now = new Date();
  const to = new Date(now);
  to.setHours(23, 59, 59, 999);
  const from = new Date(now);
  from.setDate(from.getDate() - (n - 1));
  from.setHours(0, 0, 0, 0);
  return { fromMs: from.getTime(), toMs: to.getTime() };
}

function pct(num, denom) {
  if (!denom || denom === 0) return "0.0%";
  return ((num / denom) * 100).toFixed(1) + "%";
}

function printSummary(data) {
  const from = new Date(data.period.from_ms).toLocaleDateString("en-GB");
  const to = new Date(data.period.to_ms).toLocaleDateString("en-GB");
  process.stderr.write(`\nMessaging Analytics — ${from} to ${to}\n`);
  process.stderr.write(`${"─".repeat(40)}\n`);
  process.stderr.write(`Total Sent:    ${data.total_sent}\n`);
  process.stderr.write(`Sent (Meta):   ${data.sent_count}\n`);
  process.stderr.write(`Delivered:     ${data.delivered_count}\n`);
  process.stderr.write(`Read:          ${data.read_count}\n`);
  process.stderr.write(`Delivery Rate: ${data.delivery_rate}\n`);
  process.stderr.write(`Read Rate:     ${data.read_rate}\n`);
  process.stderr.write("\n");
}

async function main() {
  const flags = parseArgs();
  const pretty = getBooleanFlag(flags, "pretty");
  const fromStr = getFlag(flags, "from");
  const toStr = getFlag(flags, "to");
  const daysRaw = getFlag(flags, "days") ?? "7";

  let fromMs, toMs;

  if (fromStr || toStr) {
    if (!fromStr || !toStr) {
      printJson(err("--from and --to must both be provided (YYYY-MM-DD)."));
      return;
    }
    fromMs = parseDateToMs(fromStr, false);
    toMs = parseDateToMs(toStr, true);
    if (!fromMs || !toMs) {
      printJson(err("Invalid date format. Use YYYY-MM-DD (e.g. 2025-01-01)."));
      return;
    }
    if (fromMs > toMs) {
      printJson(err("--from must be before or equal to --to."));
      return;
    }
  } else {
    const days = parseInt(daysRaw, 10);
    if (isNaN(days) || days < 1) {
      printJson(err("--days must be a positive integer (e.g. --days 7)."));
      return;
    }
    ({ fromMs, toMs } = getLastNDays(days));
  }

  const config = loadConfig({ requireToken: true });

  const result = await requestJson(config, {
    method: "GET",
    path: "/api:5l-RgW1B/anslytics",
    query: {
      start_timestamp: String(fromMs),
      end_timestamp: String(toMs),
    },
  });

  if (!result.ok) {
    printJson(err(result.error, result.data, false, result.status));
    return;
  }

  const raw = result.data;
  const enriched = {
    total_sent: raw.total_sent ?? 0,
    sent_count: raw.sent_count ?? 0,
    delivered_count: raw.delivered_count ?? 0,
    read_count: raw.read_count ?? 0,
    read_rate: pct(raw.read_count ?? 0, raw.total_sent ?? 0),
    delivery_rate: pct(raw.delivered_count ?? 0, raw.total_sent ?? 0),
    start_readable_time: raw.start_readable_time,
    end_readable_time: raw.end_readable_time,
    period: { from_ms: fromMs, to_ms: toMs },
  };

  if (pretty) printSummary(enriched);

  printJson(ok(enriched));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/get-template.js
content: 30 mtime=1775562702.200234305
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/get-template.js
content: #!/usr/bin/env node
/**
 * get-template.js — Retrieve a single template by name, template_id, or whatsapp_template_id.
 *
 * Internally calls GET /api:AFRA_QCy/templates_web and filters the result.
 * There is no dedicated GET-by-ID endpoint in the Notifyer API.
 *
 * Usage:
 *   node scripts/get-template.js --name order_confirmation
 *   node scripts/get-template.js --id tmpl_abc123
 *   node scripts/get-template.js --whatsapp-id 123456789
 *   node scripts/get-template.js --name order_confirmation --pretty
 *
 * Flags:
 *   --name <name>           Template name (exact match, case-insensitive)  (one required)
 *   --id <template_id>      Notifyer's internal template_id string          (one required)
 *   --whatsapp-id <num>     Meta's numeric whatsapp_template_id             (one required)
 *   --pretty                Print template details to stderr                (optional)
 *
 * Output (found):
 *   {
 *     "ok": true,
 *     "data": {
 *       "id": "abc123",
 *       "name": "order_confirmation",
 *       "template_id": "tmpl_...",
 *       "whatsapp_template_id": 123456789,
 *       "category": "MARKETING",
 *       "type": "text",
 *       "body": "Hello {{1}}, your order #{{2}} is confirmed.",
 *       "language": "en",
 *       "status": "approved",
 *       "components": [...]
 *     }
 *   }
 *
 * Output (not found):
 *   { "ok": false, "error": "Template 'foo' not found.", "data": null }
 *
 * Notes:
 *   - Uses console auth mode (Authorization: Bearer <token>).
 *   - To check if a template is usable for sending, verify status === "approved".
 *   - `components` contains the raw WhatsApp template definition with button configs,
 *     media header info, and per-variable example values.
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

function usage() {
  console.error(`
Usage:
  node scripts/get-template.js --name <name>
  node scripts/get-template.js --id <template_id>
  node scripts/get-template.js --whatsapp-id <num>

Flags:
  --name <name>         Template name (exact match, case-insensitive)
  --id <template_id>    Notifyer internal template_id string
  --whatsapp-id <num>   Meta's numeric whatsapp_template_id
  --pretty              Print details to stderr

Examples:
  node scripts/get-template.js --name order_confirmation
  node scripts/get-template.js --id tmpl_abc123
  node scripts/get-template.js --whatsapp-id 123456789
`);
  process.exit(1);
}

function printDetail(t) {
  process.stderr.write(`\nTemplate: ${t.name}\n`);
  process.stderr.write(`${"─".repeat(60)}\n`);
  process.stderr.write(`  template_id:          ${t.template_id ?? "(none)"}\n`);
  process.stderr.write(`  whatsapp_template_id: ${t.whatsapp_template_id ?? "(none)"}\n`);
  process.stderr.write(`  category:             ${t.category ?? ""}\n`);
  process.stderr.write(`  type:                 ${t.type ?? ""}\n`);
  process.stderr.write(`  language:             ${t.language ?? ""}\n`);
  process.stderr.write(`  status:               ${t.status ?? ""}\n`);
  if (t.body) {
    process.stderr.write(`  body:\n`);
    const lines = t.body.split("\n");
    for (const line of lines) {
      process.stderr.write(`    ${line}\n`);
    }
  }
  process.stderr.write("\n");
}

async function main() {
  const flags = parseArgs();
  const nameFilter = getFlag(flags, "name");
  const idFilter = getFlag(flags, "id");
  const whatsappIdFilter = getFlag(flags, "whatsapp-id");
  const pretty = getBooleanFlag(flags, "pretty");

  if (!nameFilter && !idFilter && !whatsappIdFilter) {
    console.error(
      "Error: one of --name, --id, or --whatsapp-id is required.\n"
    );
    usage();
  }

  const config = loadConfig({ requireToken: true });

  const result = await requestJson(config, {
    method: "GET",
    path: "/api:AFRA_QCy/templates_web",
  });

  if (!result.ok) {
    printJson(err(result.error, result.data, false, result.status));
  }

  const templates = Array.isArray(result.data) ? result.data : [];

  let found = null;

  if (nameFilter) {
    found = templates.find(
      (t) => (t.name ?? "").toLowerCase() === nameFilter.toLowerCase()
    );
    if (!found) {
      printJson(
        err(`Template '${nameFilter}' not found.`, null, false, 404)
      );
    }
  } else if (idFilter) {
    found = templates.find((t) => t.template_id === idFilter);
    if (!found) {
      printJson(
        err(`Template with template_id '${idFilter}' not found.`, null, false, 404)
      );
    }
  } else if (whatsappIdFilter) {
    const numId = Number(whatsappIdFilter);
    found = templates.find((t) => t.whatsapp_template_id === numId);
    if (!found) {
      printJson(
        err(
          `Template with whatsapp_template_id '${whatsappIdFilter}' not found.`,
          null,
          false,
          404
        )
      );
    }
  }

  if (pretty && found) printDetail(found);

  printJson(ok(found));
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/get-broadcast.js
content: 30 mtime=1775569127.067396442
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/get-broadcast.js
content: #!/usr/bin/env node
/**
 * get-broadcast.js — Retrieve a single broadcast by ID, name, or broadcast_identifier.
 *
 * Internally calls GET /api:6_ZYypAc/broadcast across all status groups and filters
 * client-side. There is no dedicated GET-by-ID endpoint.
 *
 * Usage:
 *   node scripts/get-broadcast.js --id 5
 *   node scripts/get-broadcast.js --name "January Sale"
 *   node scripts/get-broadcast.js --broadcast-id "uuid..."
 *   node scripts/get-broadcast.js --id 5 --pretty
 *
 *   # Search only within a specific status group (faster, avoids 3 API calls):
 *   node scripts/get-broadcast.js --id 5 --status previous
 *
 * Flags:
 *   --id <integer>            Notifyer broadcast row ID  (mutually exclusive with --name/--broadcast-id)
 *   --name <text>             Broadcast display name (case-insensitive, first match)  (optional)
 *   --broadcast-id <uuid>     broadcast_identifier UUID  (optional)
 *   --status <value>          Limit search to: upcoming | previous | ongoing  (optional, default: all)
 *   --pretty                  Print human-readable summary to stderr  (optional)
 *
 * Output (success):
 *   {
 *     "ok": true,
 *     "data": {
 *       "found_in": "upcoming",
 *       "broadcast": {
 *         "id": 5,
 *         "broadcast_name": "January Sale",
 *         "template_name": "promo_banner",
 *         "unique_numbers": "1200",
 *         "delivery_mode": "smart",
 *         "delivery_size": "4",
 *         "schedule": 1706184000000,
 *         "broadcast_identifier": "uuid...",
 *         "cost_of_broadcast": 1.44,
 *         "user_selected_read_rate": 95
 *       }
 *     }
 *   }
 *
 * Output (not found):
 *   { "ok": false, "error": "No broadcast found matching the given criteria." }
 *
 * Notes:
 *   - When --status is omitted, all three groups (upcoming, previous, ongoing) are
 *     queried in order. The first match is returned along with `found_in`.
 *   - `schedule` is a Unix millisecond timestamp.
 *   - Name match is case-insensitive and returns the first match.
 *   - Uses console auth mode (Authorization: Bearer <token>).
 *
 * Environment:
 *   NOTIFYER_API_BASE_URL   required
 *   NOTIFYER_API_TOKEN      required (from login.js)
 */

import { loadConfig, requestJson } from "./lib/notifyer-api.js";
import { parseArgs, getFlag, getBooleanFlag } from "./lib/args.js";
import { ok, err, printJson } from "./lib/result.js";

const ALL_STATUSES = ["upcoming", "previous", "ongoing"];
const CONSOLE_ORIGIN = "https://console.notifyer-systems.com";

function printSummary(broadcast, foundIn) {
  process.stderr.write(`\nBroadcast #${broadcast.id} (found in: ${foundIn})\n`);
  process.stderr.write(`${"─".repeat(60)}\n`);
  process.stderr.write(`  Name             : ${broadcast.broadcast_name ?? "—"}\n`);
  process.stderr.write(`  Template         : ${broadcast.template_name ?? "—"}\n`);
  process.stderr.write(`  Unique Numbers   : ${broadcast.unique_numbers ?? "—"}\n`);
  process.stderr.write(`  Delivery Mode    : ${broadcast.delivery_mode ?? "—"}\n`);
  process.stderr.write(`  Delivery Size    : ${broadcast.delivery_size ?? "—"}\n`);
  if (broadcast.user_selected_read_rate) {
    process.stderr.write(`  Read Rate        : ${broadcast.user_selected_read_rate}%\n`);
  }
  process.stderr.write(
    `  Schedule         : ${broadcast.schedule ? new Date(broadcast.schedule).toLocaleString() : "—"}\n`
  );
  process.stderr.write(`  Cost             : $${(broadcast.cost_of_broadcast ?? 0).toFixed(2)}\n`);
  process.stderr.write(`  Identifier       : ${broadcast.broadcast_identifier ?? "—"}\n`);
  if (broadcast.delivery_success !== undefined) {
    process.stderr.write(`  Delivered        : ${broadcast.delivery_success ?? "—"}\n`);
    process.stderr.write(`  Failed           : ${broadcast.delivery_fail ?? "—"}\n`);
    process.stderr.write(`  Sent             : ${broadcast.message_send_count ?? "—"}\n`);
    process.stderr.write(`  Read             : ${broadcast.message_read_count ?? "—"}\n`);
  }
  process.stderr.write("\n");
}

async function fetchBroadcasts(config, status) {
  const result = await requestJson(config, {
    method: "GET",
    path: "/api:6_ZYypAc/broadcast",
    query: { require: status },
    extraHeaders: { Origin: CONSOLE_ORIGIN },
  });
  if (!result.ok) return { ok: false, error: result.error, status: result.status };
  return { ok: true, data: Array.isArray(result.data) ? result.data : [] };
}

async function main() {
  const flags = parseArgs();
  const idRaw = getFlag(flags, "id");
  const name = getFlag(flags, "name");
  const broadcastUuid = getFlag(flags, "broadcast-id");
  const statusFilter = getFlag(flags, "status")?.toLowerCase();
  const pretty = getBooleanFlag(flags, "pretty");

  if (!idRaw && !name && !broadcastUuid) {
    printJson(
      err(
        "One of --id, --name, or --broadcast-id is required.\n" +
          "  Example: node scripts/get-broadcast.js --id 5"
      )
    );
    return;
  }

  const id = idRaw !== undefined ? Number(idRaw) : undefined;
  if (idRaw !== undefined && (!Number.isInteger(id) || id <= 0)) {
    printJson(err(`--id must be a positive integer, got: ${idRaw}`));
    return;
  }

  if (statusFilter && !ALL_STATUSES.includes(statusFilter)) {
    printJson(err(`--status must be one of: ${ALL_STATUSES.join(", ")}. Got: "${statusFilter}"`));
    return;
  }

  const config = loadConfig({ requireToken: true });
  const statusesToSearch = statusFilter ? [statusFilter] : ALL_STATUSES;

  for (const status of statusesToSearch) {
    const result = await fetchBroadcasts(config, status);
    if (!result.ok) {
      printJson(err(result.error, undefined, false, result.status));
      return;
    }

    let match = null;
    if (id !== undefined) {
      match = result.data.find((b) => b.id === id) ?? null;
    } else if (broadcastUuid) {
      match = result.data.find((b) => b.broadcast_identifier === broadcastUuid) ?? null;
    } else if (name) {
      const needle = name.toLowerCase();
      match = result.data.find((b) => (b.broadcast_name ?? "").toLowerCase() === needle) ?? null;
    }

    if (match) {
      if (pretty) printSummary(match, status);
      printJson(ok({ found_in: status, broadcast: match }));
      return;
    }
  }

  const criteria = idRaw
    ? `id ${idRaw}`
    : broadcastUuid
    ? `broadcast_identifier "${broadcastUuid}"`
    : `name "${name}"`;
  printJson(
    err(
      `No broadcast found matching the given criteria (${criteria})` +
        (statusFilter ? ` in status "${statusFilter}".` : " across upcoming, previous, and ongoing.")
    )
  );
}

main().catch((e) => {
  printJson(err(`Unexpected error: ${e.message}`));
});


path: skills/automate-notifyer/scripts/PaxHeader/get-message-logs.js
content: 30 mtime=1775571913.095578252
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/PaxHeader/delete-bot.js
content: 30 mtime=1775712586.790152359
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/PaxHeader/update-bot.js
content: 30 mtime=1775712571.974698701
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/PaxHeader/list-bots.js
content: 29 mtime=1775565367.45791352
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/PaxHeader/delete-broadcast.js
content: 30 mtime=1775712616.964561304
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/PaxHeader/create-bot.js
content: 30 mtime=1775565435.842731835
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/PaxHeader/set-default-bot.js
content: 30 mtime=1775712598.248027529
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/lib/PaxHeader/notifyer-api.js
content: 28 mtime=1775716013.6288296
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/lib/PaxHeader/result.js
content: 29 mtime=1775569591.95650889
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/scripts/lib/result.js
content: /**
 * Standard output helpers.
 *
 * All scripts print a single JSON line to stdout so that calling agents
 * can reliably parse the result without screen-scraping.
 *
 * Success: { "ok": true, "data": { ... } }
 * Failure: { "ok": false, "error": "...", "details": ..., "blocked": false }
 */

/**
 * Build a success result.
 * @param {any} data
 * @returns {{ ok: true, data: any }}
 */
export function ok(data) {
  return { ok: true, data };
}

/**
 * Build an error result.
 * @param {string} message
 * @param {any} [details]
 * @param {boolean} [blocked=false]  true if action is blocked and agent should not retry
 * @param {number} [status]          HTTP status code if applicable
 * @returns {{ ok: false, error: string, details?: any, blocked: boolean, status?: number }}
 */
export function err(message, details, blocked = false, status) {
  const result = { ok: false, error: message, blocked };
  if (details !== undefined) result.details = details;
  if (status !== undefined) result.status = status;
  return result;
}

/**
 * Print a result object as a single JSON line to stdout and exit.
 * @param {{ ok: boolean }} value
 */
export function printJson(value) {
  process.stdout.write(JSON.stringify(value) + "\n");
  process.exit(value.ok ? 0 : 1);
}


path: skills/automate-notifyer/scripts/lib/PaxHeader/args.js
content: 30 mtime=1775569591.954820265
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


path: skills/automate-notifyer/references/PaxHeader/broadcasts-reference.md
content: 30 mtime=1776964078.743887402
57 LIBARCHIVE.xattr.com.apple.provenance=AQIA3DAHxqzCVe8
49 SCHILY.xattr.com.apple.provenance=   �0 Ƭ�U�


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