{
  "name": "Paperclip Events → Telegram Notifier",
  "nodes": [
    {
      "id": "schedule",
      "name": "Every 2 min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [240, 300],
      "parameters": {
        "rule": {
          "interval": [
            { "field": "minutes", "minutesInterval": 2 }
          ]
        }
      }
    },
    {
      "id": "fetch-activity",
      "name": "Fetch Paperclip Activity",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [440, 300],
      "parameters": {
        "method": "GET",
        "url": "=https://paperclip.galhardo.cloud/api/companies/{{ $env.PAPERCLIP_COMPANY_ID }}/activity?limit=200",
        "authentication": "none",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Authorization", "value": "=Bearer {{ $env.PAPERCLIP_NOTIFIER_KEY }}" }
          ]
        },
        "options": { "response": { "response": { "neverError": true, "responseFormat": "json" } } }
      },
      "alwaysOutputData": true
    },
    {
      "id": "filter-and-format",
      "name": "Filter + Format",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [640, 300],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Paperclip activity API doesn't honor ?since=. Client-filter + resolve IDs to names.\n// n8n splits HTTP array responses into items — flatten via $input.all().\n\nconst ALLOW = new Set([\n  'agent.hire_created', 'agent.approved', 'agent.rejected', 'agent.paused',\n  'agent.resumed', 'agent.key_created',\n  'approval.created', 'approval.approved', 'approval.rejected',\n  'issue.created', 'issue.updated', 'issue.blocked', 'issue.closed',\n  'issue.completed', 'issue.released',\n  'run.failed', 'run.errored',\n]);\n\n// Hardcoded ID → name map (authoritative, small set, stable). Covers Joao + all current\n// Paperclip agents. Unknown IDs fall back to short hex. Refresh this list manually\n// when new agents are hired; or if this becomes painful, extend to an async roster fetch.\nconst ACTOR_NAMES = {\n  '1UaxRv3wk0Z9eMeaR830GTJcDSpW81Ez': 'joão',\n  '85f36186-eba3-48ab-b884-b2523e397bf6': 'claude',\n  '10e5d574-58d2-4662-8f4a-25dd9ef5f5f2': 'joao-agent',\n  '998dbb1c-ae17-4645-92a3-f7e1e633f525': 'ana',\n  '16ceb5ca-d76a-4802-b96c-36bb8d840187': 'manuel',\n  '69e6c009-57c0-4200-b364-5afc48180947': 'camila',\n  '4b49d9c3-b8ae-4af8-9f2f-1f28374da4e2': 'openrouter-fleet',\n  '8b6efea8-ecdd-4d46-98c3-842bc746cbaa': 'dev',\n  'fdb1ea75-f291-43ef-b674-55a16088775a': 'devops-sentinel',\n  '3b00c249-5f8a-4d07-9de5-6e567f0084af': 'strategos',\n  'f08106dd-2712-46b1-98c0-35e99fc9cea5': 'beacon',\n};\nconst resolveActor = id => {\n  if (!id) return '-';\n  return ACTOR_NAMES[id] || id.slice(0, 8);\n};\n\nconst staticData = $getWorkflowStaticData('global');\nconst nowIso = new Date().toISOString();\nconst lastIso = staticData.lastPollAt || new Date(Date.now() - 5 * 60 * 1000).toISOString();\n\nconst events = $input.all()\n  .map(i => i.json)\n  .filter(j => j && typeof j === 'object' && j.action);\n\nconst fresh = events.filter(e =>\n  ALLOW.has(e.action) && e.createdAt && e.createdAt > lastIso\n);\n\n// De-dup: per entity, keep only the LATEST event per action. An issue that's\n// created then updated twice still shows 1 create + 1 update (latest), not 3 lines.\nfresh.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\nconst dedupKey = e => `${e.entityId || '-'}:${e.action}`;\nconst byKey = new Map();\nfor (const e of fresh) byKey.set(dedupKey(e), e);\nconst deduped = [...byKey.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n\nconst emojiFor = action => {\n  if (action === 'agent.hire_created') return '🆕';\n  if (action === 'agent.approved' || action === 'approval.approved') return '✅';\n  if (action === 'agent.rejected' || action === 'approval.rejected') return '❌';\n  if (action === 'agent.paused') return '⏸️';\n  if (action === 'agent.resumed') return '▶️';\n  if (action === 'agent.key_created') return '🔑';\n  if (action === 'approval.created') return '⏳';\n  if (action.startsWith('issue.')) return '📌';\n  if (action.startsWith('run.')) return '🔴';\n  return '📋';\n};\n\nconst lines = deduped.map(e => {\n  const emoji = emojiFor(e.action);\n  const actor = resolveActor(e.actorId);\n  const when = e.createdAt ? e.createdAt.slice(11, 19) : '?';\n  const d = e.details && typeof e.details === 'object' ? e.details : {};\n  \n  // Issue events: include clickable GAL-N key + title + status-transition if applicable\n  if (e.action.startsWith('issue.') && d.identifier) {\n    const keyLink = `[${d.identifier}](https://paperclip.galhardo.cloud/issues/${d.identifier})`;\n    let detail = '';\n    if (e.action === 'issue.created' && d.title) {\n      detail = ` — _${String(d.title).replace(/[_*`[\\]]/g, '').slice(0, 60)}_`;\n    } else if (e.action === 'issue.updated' && d.status) {\n      const prev = d._previous && d._previous.status ? `${d._previous.status} → ` : '';\n      detail = ` | ${prev}*${d.status}*`;\n    } else if (d.status) {\n      detail = ` | status=${d.status}`;\n    }\n    return `${when} ${emoji} ${e.action.replace('issue.', '')} ${keyLink}${detail} · by ${actor}`;\n  }\n  \n  // Agent/approval/run events\n  let extra = '';\n  if (d.name) extra += ` name=${d.name}`;\n  if (d.role) extra += ` role=${d.role}`;\n  if (d.status && !extra.includes('status=')) extra += ` status=${d.status}`;\n  if (d.reason) extra += ` reason=\"${String(d.reason).replace(/[_*`[\\]]/g, '').slice(0, 50)}\"`;\n  const verb = e.action.split('.').slice(1).join('.');\n  const scope = e.action.split('.')[0];\n  return `${when} ${emoji} ${scope} ${verb}${extra} · by ${actor}`;\n});\n\nstaticData.lastPollAt = nowIso;\n\nif (lines.length === 0) return [];\n\nconst header = `📋 Paperclip — ${lines.length} event${lines.length > 1 ? 's' : ''}`;\nreturn [{ json: { text: `${header}\\n\\n${lines.join('\\n')}`, eventCount: lines.length } }];\n"
      }
    },
    {
      "id": "send-telegram",
      "name": "Send Telegram Notification",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [840, 300],
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN_OPS }}/sendMessage",
        "authentication": "none",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Content-Type", "value": "application/json" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({chat_id: $env.TELEGRAM_CHAT_ID, text: $json.text, parse_mode: 'Markdown', disable_web_page_preview: true}) }}",
        "options": {}
      }
    }
  ],
  "connections": {
    "Every 2 min": { "main": [[{ "node": "Fetch Paperclip Activity", "type": "main", "index": 0 }]] },
    "Fetch Paperclip Activity": { "main": [[{ "node": "Filter + Format", "type": "main", "index": 0 }]] },
    "Filter + Format": { "main": [[{ "node": "Send Telegram Notification", "type": "main", "index": 0 }]] }
  },
  "settings": {
    "executionOrder": "v1",
    "saveExecutionProgress": false,
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromSameOwner"
  }
}
