1/* 2 * service-catalog.js â frontend mirror of the backend service catalog. 3 * 4 * Mirrors KEYS, labels, icons and defaultType from 5 * backend/src/services/automation/service-catalog.js, plus CATEGORY_META 6 * colours from graph.js. The canvas, palette and inspector all resolve a 7 * service's icon + accent colour through here. 8 * 9 * AI agents & LangChain sub-nodes: a handful of services (aiAgent, the chat 10 * models, memories, tools, output parser) describe an n8n "cluster": an 11 * `aiAgent` root with model / memory / tool / parser sub-nodes hanging beneath 12 * it, attached through typed ai_* connection ports. Sub-node services carry a 13 * `subRole` ('languageModel' | 'memory' | 'tool' | 'outputParser'); helpers 14 * below resolve that role -> the ai_* port it attaches through and the caption 15 * shown under the agent. Mirrors AI_PORTS / SUBROLE_TO_PORT / subRoleOf on the 16 * backend exactly so generated graphs render identically. 17 * 18 * Exposes: 19 * window.DP_AUTO_CATALOG = { 20 * services: { key: { label, icon, defaultType, subRole? } }, 21 * CATEGORY_META: { type: { color, glyph } }, 22 * AI_PORTS: [ ... ], PORT_LABELS: { port: 'Chat Model' ... }, 23 * iconSvg(iconKey, size?) -> string (inline <svg>), 24 * get(key), list(), colorForType(type), 25 * isAiPort(port), subRoleOf(key), portForService(key), portLabel(port) 26 * } 27 */ 28(function () { 29 'use strict'; 30 31 // Category accent colour + glyph. Colours match the backend CATEGORY_META so 32 // a node looks identical on the canvas and in the exported native file. 33 var CATEGORY_META = { 34 trigger: { color: '#16a34a', glyph: 'â¶' }, // â¶ 35 action: { color: '#2563eb', glyph: 'â' }, // â 36 logic: { color: '#d97706', glyph: 'â' }, // â 37 transform: { color: '#7c3aed', glyph: 'â' }, // â 38 output: { color: '#0d9488', glyph: 'â' }, // â 39 loop: { color: '#d97706', glyph: 'â»' }, // â» control-flow (amber) 40 note: { color: '#ca8a04', glyph: 'â' }, // â sticky-note (parchment) 41 }; 42 43 // Typed connection ports that attach a LangChain sub-node to an AI-agent 44 // parent. An edge whose `fromPort` is one of these marks its `from` node as a 45 // sub-node feeding INTO the `to` node (the agent). Mirrors backend AI_PORTS. 46 var AI_PORTS = ['ai_languageModel', 'ai_memory', 'ai_tool', 'ai_outputParser', 'ai_embedding']; 47 var AI_PORT_SET = {}; 48 AI_PORTS.forEach(function (p) { AI_PORT_SET[p] = true; }); 49 50 // subRole -> the ai_* port it attaches through (mirror of SUBROLE_TO_PORT). 51 var SUBROLE_TO_PORT = { 52 languageModel: 'ai_languageModel', 53 memory: 'ai_memory', 54 tool: 'ai_tool', 55 outputParser: 'ai_outputParser', 56 embedding: 'ai_embedding', 57 }; 58 59 // The caption shown beneath an agent at each bottom port. 60 var PORT_LABELS = { 61 ai_languageModel: 'Chat Model', 62 ai_memory: 'Memory', 63 ai_tool: 'Tool', 64 ai_outputParser: 'Output Parser', 65 ai_embedding: 'Embedding', 66 }; 67 68 // Service registry â keys + labels + icon key + default node type, mirroring 69 // the backend catalog one-for-one. Sub-node services additionally carry a 70 // `subRole`. Platform id maps live server-side; the frontend only needs what 71 // it renders. 72 var SERVICES = { 73 webhook: { label: 'Webhook', icon: 'webhook', defaultType: 'trigger' }, 74 schedule: { label: 'Schedule', icon: 'clock', defaultType: 'trigger' }, 75 manual: { label: 'Manual Trigger', icon: 'play', defaultType: 'trigger' }, 76 http: { label: 'HTTP Request', icon: 'globe', defaultType: 'action' }, 77 code: { label: 'Code', icon: 'code', defaultType: 'transform' }, 78 filter: { label: 'Filter / IF', icon: 'filter', defaultType: 'logic' }, 79 router: { label: 'Router / Switch', icon: 'switch', defaultType: 'logic' }, 80 set: { label: 'Edit Fields', icon: 'edit', defaultType: 'transform' }, 81 merge: { label: 'Merge', icon: 'merge', defaultType: 'transform' }, 82 delay: { label: 'Delay / Wait', icon: 'pause', defaultType: 'action' },
83 respond: { label: 'Respond to Webhook', icon: 'reply', defaultType: 'output' }, 84 slack: { label: 'Slack', icon: 'slack', defaultType: 'action' }, 85 gmail: { label: 'Gmail', icon: 'gmail', defaultType: 'action' }, 86 email: { label: 'Email (SMTP)', icon: 'mail', defaultType: 'action' }, 87 googleSheets: { label: 'Google Sheets', icon: 'sheet', defaultType: 'action' }, 88 googleCalendar: { label: 'Google Calendar', icon: 'calendar', defaultType: 'action' }, 89 googleDrive: { label: 'Google Drive', icon: 'drive', defaultType: 'action' }, 90 airtable: { label: 'Airtable', icon: 'airtable', defaultType: 'action' }, 91 notion: { label: 'Notion', icon: 'notion', defaultType: 'action' }, 92 openai: { label: 'OpenAI', icon: 'openai', defaultType: 'action' }, 93 telegram: { label: 'Telegram', icon: 'telegram', defaultType: 'action' }, 94 discord: { label: 'Discord', icon: 'discord', defaultType: 'action' }, 95 stripe: { label: 'Stripe', icon: 'stripe', defaultType: 'trigger' }, 96 shopify: { label: 'Shopify', icon: 'shopify', defaultType: 'trigger' }, 97 hubspot: { label: 'HubSpot', icon: 'hubspot', defaultType: 'action' }, 98 postgres: { label: 'Postgres', icon: 'postgres', defaultType: 'action' }, 99 twilio: { label: 'Twilio SMS', icon: 'twilio', defaultType: 'action' }, 100 github: { label: 'GitHub', icon: 'github', defaultType: 'trigger' }, 101 102 // ââ Security-ops / SOAR + API automation (HTTP-API integrations) âââââââââ 103 virustotal: { label: 'VirusTotal', icon: 'virustotal', defaultType: 'action' }, 104 abuseipdb: { label: 'AbuseIPDB', icon: 'shieldAlert', defaultType: 'action' }, 105 shodan: { label: 'Shodan', icon: 'shodan', defaultType: 'action' }, 106 urlscan: { label: 'urlscan.io', icon: 'scan', defaultType: 'action' }, 107 greynoise: { label: 'GreyNoise', icon: 'shield', defaultType: 'action' }, 108 pagerduty: { label: 'PagerDuty', icon: 'pager', defaultType: 'output' }, 109 opsgenie: { label: 'Opsgenie', icon: 'bell', defaultType: 'output' }, 110 siem: { label: 'SIEM / Splunk HEC', icon: 'funnel', defaultType: 'output' }, 111 apiRequest: { label: 'API Request', icon: 'api', defaultType: 'action' }, 112 113 // ââ AI agents & LangChain sub-nodes (mirror the backend keys) ââââââââââââ 114 aiAgent: { label: 'AI Agent', icon: 'aiagent', defaultType: 'action' }, 115 chatModelAnthropic: { label: 'Anthropic Chat Model', icon: 'anthropic', defaultType: 'action', subRole: 'languageModel' }, 116 chatModelOpenai: { label: 'OpenAI Chat Model', icon: 'openai', defaultType: 'action', subRole: 'languageModel' }, 117 chatModelGemini: { label: 'Google Gemini Chat Model', icon: 'gemini', defaultType: 'action', subRole: 'languageModel' }, 118 memoryWindow: { label: 'Simple Memory', icon: 'memory', defaultType: 'action', subRole: 'memory' }, 119 memoryPostgres: { label: 'Postgres Chat Memory', icon: 'postgres', defaultType: 'action', subRole: 'memory' }, 120 toolHttp: { label: 'HTTP Request Tool', icon: 'globe', defaultType: 'action', subRole: 'tool' }, 121 toolWorkflow: { label: 'Call n8n Workflow Tool', icon: 'workflow', defaultType: 'action', subRole: 'tool' }, 122 toolCode: { label: 'Code Tool', icon: 'code', defaultType: 'action', subRole: 'tool' }, 123 tool: { label: 'Agent Tool', icon: 'tool', defaultType: 'action', subRole: 'tool' }, 124 outputParser: { label: 'Structured Output Parser', icon: 'braces', defaultType: 'transform', subRole: 'outputParser' }, 125 chatTrigger: { label: 'Chat Trigger', icon: 'chat', defaultType: 'trigger' }, 126 formTrigger: { label: 'Form Trigger', icon: 'form', defaultType: 'trigger' }, 127 mcpTrigger: { label: 'MCP Server Trigger', icon: 'chat', defaultType: 'trigger' }, 128 jira: { label: 'Jira', icon: 'jira', defaultType: 'action' }, 129
130 // ââ Control-flow & infrastructure (mirror the backend keys) ââââââââââââââ 131 // `category` is a coarse grouping the palette can section by; loops/notes 132 // additionally surface a `nodeType` so the canvas can branch on them without 133 // re-deriving from the key. 134 loop: { label: 'Loop Over Items', icon: 'loop', defaultType: 'logic', category: 'flow', nodeType: 'loop' }, 135 note: { label: 'Sticky Note', icon: 'note', defaultType: 'note', category: 'annotation', nodeType: 'note' }, 136 stickyNote: { label: 'Sticky Note', icon: 'note', defaultType: 'note', category: 'annotation', nodeType: 'note' }, 137 errorTrigger: { label: 'Error Trigger', icon: 'alertTriangle', defaultType: 'trigger', category: 'flow' }, 138 dataStore: { label: 'Data Store', icon: 'dataStore', defaultType: 'action', category: 'infra' }, 139 140 // ââ Feeds & inbound email ââââââââââââââââââââââââââââââââââââââââââââââââ 141 rss: { label: 'RSS Feed', icon: 'rss', defaultType: 'trigger' }, 142 emailTrigger: { label: 'Email Trigger (IMAP)', icon: 'inbox', defaultType: 'trigger' }, 143 144 // ââ Native connectors (mirror the backend keys) ââââââââââââââââââââââââââ 145 salesforce: { label: 'Salesforce', icon: 'salesforce', defaultType: 'action' }, 146 mongodb: { label: 'MongoDB', icon: 'mongodb', defaultType: 'action' }, 147 mysql: { label: 'MySQL', icon: 'mysql', defaultType: 'action' }, 148 s3: { label: 'AWS S3', icon: 's3', defaultType: 'action' }, 149 sftp: { label: 'SFTP / FTP', icon: 'sftp', defaultType: 'action' }, 150 monday: { label: 'monday.com', icon: 'monday', defaultType: 'action' }, 151 mailchimp: { label: 'Mailchimp', icon: 'mailchimp', defaultType: 'action' }, 152 microsoftTeams: { label: 'Microsoft Teams', icon: 'teams', defaultType: 'action' }, 153 microsoftOutlook:{ label: 'Microsoft Outlook', icon: 'outlook', defaultType: 'action' }, 154 trello: { label: 'Trello', icon: 'trello', defaultType: 'action' }, 155 asana: { label: 'Asana', icon: 'asana', defaultType: 'action' }, 156 linear: { label: 'Linear', icon: 'linear', defaultType: 'action' }, 157 158 generic: { label: 'Step', icon: 'box', defaultType: 'action' }, 159 }; 160 161 // ââ Icon library âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 162 // Two flavours of inline SVG mark: 163 // ⢠Stroke glyphs (default) â drawn with currentColor so the chip tints them 164 // to the category accent. 24x24 viewBox, 1.8 stroke. 165 // ⢠Brand marks â recognizable service logos. Some are flagged `brand:true` 166 // so they keep their own fill colours (Slack, Stripe, Notion, â¦) instead 167 // of inheriting the accent; the rest tint via currentColor like a glyph. 168 // Each entry is { d } for a tinted stroke glyph, or { raw, brand } for a mark 169 // whose inner markup is emitted verbatim (its own fills / multiple colours). 170 var ICONS = { 171 // Core primitives (tinted stroke glyphs). 172 webhook: { d: '<path d="M9 8a3 3 0 1 1 4.2 2.7l2.6 4.5"/><circle cx="6.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/><path d="M9 17.5h6"/><path d="M8.2 10.7 5.4 15.2"/>' }, 173 clock: { d: '<circle cx="12" cy="12" r="8"/><path d="M12 8v4l2.5 2"/>' }, 174 play: { d: '<path d="M8 6.5v11l9-5.5z"/>' }, 175 globe: { d: '<circle cx="12" cy="12" r="8"/><path d="M4 12h16"/><path d="M12 4c2.5 2.2 2.5 13.8 0 16M12 4c-2.5 2.2-2.5 13.8 0 16"/>' }, 176 code: { d: '<path d="m8 9-3 3 3 3"/><path d="m16 9 3 3-3 3"/><path d="m13 7-2 10"/>' }, 177 filter: { d: '<path d="M4 5h16l-6 7v5l-4 2v-9z"/>' }, 178 switch: { d: '<path d="M4 8h7l3 8h6"/><path d="M4 16h7"/><path d="m17 5 3 3-3 3"/>' }, 179 edit: { d: '<path d="M4 16.5 14.5 6l3.5 3.5L7.5 20H4z"/><path d="M13 7.5 16.5 11"/>' }, 180 merge: { d: '<path d="M7 4v5a4 4 0 0 0 4 4h7"/><path d="M7 20v-5"/><path d="m15 9 3 4-3 4"/>' }, 181 pause: { d: '<rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>' }, 182 reply: { d: '<path d="M9 7 4 12l5 5"/><path d="M4 12h9a6 6 0 0 1 6 6v1"/>' }, 183 mail: { d: '<rect x="3.5" y="6" width="17" height="12" rx="2"/><path d="m4 8 8 5 8-5"/>' }, 184 table: { d: '<rect x="4" y="5" width="16" height="14" rx="2"/><path d="M4 10h16M10 5v14"/>' }, 185 database: { d: '<ellipse cx="12" cy="6.5" rx="7" ry="2.6"/><path d="M5 6.5v11c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6v-11"/><path d="M5 12c0 1.4 3.1 2.6 7 2.6s7-1.2 7-2.6"/>' }, 186 sms: { d: '<path d="M5 6h14a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 19 16h-6l-4 3v-3H5a1.5 1.5 0 0 1-1.5-1.5v-7A1.5 1.5 0 0 1 5 6z"/><path d="M8 11h.01M12 11h.01M16 11h.01"/>' }, 187 box: { d: '<path d="M12 4 4 8v8l8 4 8-4V8z"/><path d="M4 8l8 4 8-4M12 12v8"/>' }, 188 form: { d: '<rect x="4.5" y="3.5" width="15" height="17" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/>' }, 189 chat: { d: '<path d="M5 5h14a1.5 1.5 0 0 1 1.5 1.5v8A1.5 1.5 0 0 1 19 16H10l-4 3.5V16H5a1.5 1.5 0 0 1-1.5-1.5v-8A1.5 1.5 0 0 1 5 5z"/><path d="M8.5 9.5h7M8.5 12h4.5"/>' }, 190 workflow: { d: '<rect x="3.5" y="4" width="6" height="6" rx="1.5"/><rect x="14.5" y="4" width="6" height="6" rx="1.5"/><rect x="9" y="14.5" width="6" height="6" rx="1.5"/><path d="M6.5 10v2.5a2 2 0 0 0 2 2H10M17.5 10v2.5a2 2 0 0 1-2 2H14"/>' }, 191 tool: { d: '<path d="M15.5 4.5a4 4 0 0 0-5.2 5.1L4.5 15.4a2 2 0 1 0 2.8 2.8l5.8-5.8a4 4 0 0 0 5.1-5.2l-2.5 2.5-2.3-.6-.6-2.3z"/>' }, 192 braces: { d: '<path d="M9 4.5c-2 0-2 2.5-2 3.5s0 2.5-2 3.5c2 1 2 2.5 2 3.5s0 3.5 2 3.5"/><path d="M15 4.5c2 0 2 2.5 2 3.5s0 2.5 2 3.5c-2 1-2 2.5-2 3.5s0 3.5-2 3.5"/>' }, 193 memory: { d: '<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 3.5v2.5M12 3.5v2.5M15 3.5v2.5M9 18v2.5M12 18v2.5M15 18v2.5M3.5 9H6M3.5 12H6M3.5 15H6M18 9h2.5M18 12h2.5M18 15h2.5"/><rect x="9.5" y="9.5" width="5" height="5" rx="1"/>' }, 194
195 // ââ Control-flow & infrastructure glyphs (tinted stroke) ââââââââââââââââââââ 196 // Loop â a repeat/refresh arrow circling back on itself. 197 loop: { d: '<path d="M5 9a7 7 0 0 1 11.5-3.2L19 8"/><path d="M19 4v4h-4"/><path d="M19 15a7 7 0 0 1-11.5 3.2L5 16"/><path d="M5 20v-4h4"/>' }, 198 // Sticky note â a page with a folded bottom-right corner. 199 note: { d: '<path d="M5 4.5h14v9l-5.5 5.5H5z"/><path d="M19 13.5h-5.5V19"/>' }, 200 // Alert triangle â error / error-trigger. 201 alertTriangle: { d: '<path d="M12 4.5 21 19H3z"/><path d="M12 10v4M12 16.6h.01"/>' }, 202 // Data store â a database cylinder with a small key/check, for dedup/state. 203 dataStore: { d: '<ellipse cx="12" cy="6.3" rx="6.5" ry="2.4"/><path d="M5.5 6.3v6.4c0 1.3 2.9 2.4 6.5 2.4M5.5 9.5c0 1.3 2.9 2.4 6.5 2.4"/><path d="M14.5 17.5h4M16.5 15.5v4" stroke-width="1.6"/>' }, 204 // RSS â the broadcast dot + two arcs. 205 rss: { d: '<circle cx="6.5" cy="17.5" r="1.6"/><path d="M5 11a8 8 0 0 1 8 8"/><path d="M5 5a14 14 0 0 1 14 14"/>' }, 206 // Inbox / inbound email â a tray with an incoming arrow. 207 inbox: { d: '<path d="M4 13.5 6 6h12l2 7.5v4.5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"/><path d="M4 13.5h4l1.2 2.2h5.6L16 13.5h4"/>' }, 208 // SFTP/FTP â a server stack with an up/down transfer arrow. 209 sftp: { d: '<rect x="4.5" y="5" width="15" height="5" rx="1.5"/><rect x="4.5" y="14" width="15" height="5" rx="1.5"/><path d="M7.5 7.5h.01M7.5 16.5h.01"/><path d="M13.5 12v-1.5M13.5 12v1.5" stroke-width="1.6"/>' }, 210 211 // ââ Security-ops / SOAR + API glyphs (tinted stroke) ââââââââââââââââââââââââ 212 // Plain shield â threat-intel reputation lookups (GreyNoise, fallback). 213 shield: { d: '<path d="M12 3.5 5 6v5c0 4.2 2.9 7.5 7 9 4.1-1.5 7-4.8 7-9V6z"/>' }, 214 // Shield with an alert bang â IP-abuse / blocklist (AbuseIPDB). 215 shieldAlert: { d: '<path d="M12 3.5 5 6v5c0 4.2 2.9 7.5 7 9 4.1-1.5 7-4.8 7-9V6z"/><path d="M12 8.5v4M12 15.4h.01"/>' }, 216 // Sweeping radar â host/port surface intel (Shodan fallback glyph). 217 radar: { d: '<path d="M12 12 18 7"/><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="4.2"/><path d="M12 4a8 8 0 0 1 7 4"/>' }, 218 // Magnifier over a globe â URL analysis (urlscan.io). 219 scan: { d: '<circle cx="11" cy="11" r="6.5"/><path d="M4.5 11h13M11 4.5c2 1.9 2 11.1 0 13M11 4.5c-2 1.9-2 11.1 0 13"/><path d="m16 16 3.5 3.5"/>' }, 220 // Pager handset â on-call paging (PagerDuty fallback glyph). 221 pager: { d: '<rect x="3.5" y="6" width="17" height="12" rx="2"/><path d="M7 10h6M7 13.5h4"/><rect x="15.5" y="9.5" width="2.5" height="5" rx="1"/>' }, 222 // Alert bell â alerting (Opsgenie). 223 bell: { d: '<path d="M6.5 16V11a5.5 5.5 0 0 1 11 0v5l1.5 2h-14z"/><path d="M10 18.5a2 2 0 0 0 4 0"/>' }, 224 // Funnel into a stream â events into a SIEM collector (Splunk HEC). 225 funnel: { d: '<path d="M4 5h16l-6 7v6l-4-2v-4z"/><path d="M9 20h6"/>' }, 226 // Globe wrapped in braces â a generic authenticated API call (apiRequest). 227 api: { d: '<circle cx="12" cy="12" r="5.2"/><path d="M6.8 12h10.4M12 6.8c1.6 1.4 1.6 8.6 0 10.4M12 6.8c-1.6 1.4-1.6 8.6 0 10.4"/><path d="M7 4.5C5 4.5 5 7 5 8s0 2-1.5 2.5C5 11 5 12 5 13M17 4.5c2 0 2 2.5 2 3.5s0 2 1.5 2.5C19 11 19 12 19 13" stroke-width="1.5"/>' }, 228 229 // ââ Brand marks âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 230 // AI agent: a friendly robot head + spark, in Anthropic's clay. Reads as a 231 // distinct "agent" the way the n8n screenshot's agent node does. 232 aiagent: { brand: true, raw: 233 '<rect x="4.5" y="7" width="15" height="11" rx="3.2" fill="#D77655"/>' + 234 '<path d="M12 3.4v3.2" stroke="#D77655" stroke-width="1.8" stroke-linecap="round"/>' + 235 '<circle cx="12" cy="3" r="1.5" fill="#D77655"/>' + 236 '<circle cx="9.2" cy="12.4" r="1.5" fill="#fff"/><circle cx="14.8" cy="12.4" r="1.5" fill="#fff"/>' + 237 '<path d="M9.5 15.4h5" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/>' + 238 '<path d="M3.4 11v3M20.6 11v3" stroke="#D77655" stroke-width="1.8" stroke-linecap="round"/>' }, 239 240 // Anthropic â the burst/asterisk mark in clay. 241 anthropic: { brand: true, raw: 242 '<g fill="#D77655">' + 243 '<path d="M13.7 4.5h2.9l5.1 15h-3l-1.1-3.3h-5.3l1.1-2.7h3.3l-2-5.9z"/>' + 244 '<path d="M7.4 4.5h3l5.1 15h-3l-1-3.1H6.3l-1 3.1h-3z" opacity=".88"/>' + 245 '</g>' }, 246 247 // OpenAI â the knot mark. 248 openai: { brand: true, raw: 249 '<path fill="#000" d="M21 10.2a4.5 4.5 0 0 0-.4-3.7 4.6 4.6 0 0 0-4.9-2.2A4.5 4.5 0 0 0 12.3 3a4.6 4.6 0 0 0-4.4 3.2 4.5 4.5 0 0 0-3 2.2A4.6 4.6 0 0 0 5.5 14a4.5 4.5 0 0 0 .4 3.7 4.6 4.6 0 0 0 4.9 2.2A4.5 4.5 0 0 0 14.1 21a4.6 4.6 0 0 0 4.4-3.2 4.5 4.5 0 0 0 3-2.2 4.6 4.6 0 0 0-.5-5.4zM13.9 19.4a3.4 3.4 0 0 1-2.2-.8l2.9-1.7a.5.5 0 0 0 .2-.4v-4.1l1.2.7v3.4a3.4 3.4 0 0 1-3.4 3.4l1.3-.5zM7.2 16.5a3.4 3.4 0 0 1-.4-2.3l2.9 1.7a.5.5 0 0 0 .5 0l3.6-2v1.4l-3 1.8a3.4 3.4 0 0 1-4.1-.6l.5.4zM6 8.8a3.4 3.4 0 0 1 1.8-1.5v3.5a.5.5 0 0 0 .2.4l3.6 2-1.2.7-3-1.7A3.4 3.4 0 0 1 6 8.8zm10.3 2.4-3.6-2 1.2-.7 3 1.7a3.4 3.4 0 0 1-.5 6.2v-3.5a.5.5 0 0 0-.1-.4zm1.2-1.8L14.6 7.7a.5.5 0 0 0-.5 0l-3.6 2V8.3l3-1.8a3.4 3.4 0 0 1 5 3.5l-.4-.6zM9.8 12.3l-1.2-.7V8.2a3.4 3.4 0 0 1 5.6-2.6L11.3 7.3a.5.5 0 0 0-.2.4l-.1 4.6zm.7-1.4 1.6-.9 1.6.9v1.9l-1.6.9-1.6-.9z"/>' }, 250 251 // Google Gemini â four-point spark in Google blue. 252 gemini: { brand: true, raw: 253 '<path fill="#4285F4" d="M12 3c.3 4.6 4.4 8.7 9 9-4.6.3-8.7 4.4-9 9-.3-4.6-4.4-8.7-9-9 4.6-.3 8.7-4.4 9-9z"/>' }, 254 255 // Slack â four-colour hash. 256 slack: { brand: true, raw: 257 '<g stroke-linecap="round">' + 258 '<path d="M9.6 4.5a1.6 1.6 0 1 0-1.6 1.6h1.6z" fill="#36C5F0"/><path d="M9.6 7.2H5.2a1.6 1.6 0 1 0 0 1.6h4.4z" fill="#36C5F0"/>' + 259 '<path d="M19.5 9.6a1.6 1.6 0 1 0-1.6-1.6v1.6z" fill="#2EB67D"/>
259<path d="M16.8 9.6V5.2a1.6 1.6 0 1 0-1.6 0v4.4z" fill="#2EB67D"/>' + 260 '<path d="M14.4 19.5a1.6 1.6 0 1 0 1.6-1.6h-1.6z" fill="#ECB22E"/><path d="M14.4 16.8h4.4a1.6 1.6 0 1 0 0-1.6h-4.4z" fill="#ECB22E"/>' + 261 '<path d="M4.5 14.4a1.6 1.6 0 1 0 1.6 1.6v-1.6z" fill="#E01E5A"/><path d="M7.2 14.4v4.4a1.6 1.6 0 1 0 1.6 0v-4.4z" fill="#E01E5A"/>' + 262 '</g>' }, 263 264 // Gmail â the red/white envelope. 265 gmail: { brand: true, raw: 266 '<rect x="3.5" y="5.5" width="17" height="13" rx="2" fill="#fff" stroke="#EA4335" stroke-width="1.4"/>' + 267 '<path d="M4 6.5 12 12.5 20 6.5" fill="none" stroke="#EA4335" stroke-width="1.6" stroke-linejoin="round"/>' + 268 '<path d="M3.8 18V8l3 2.2V18z" fill="#34A853"/><path d="M20.2 18V8l-3 2.2V18z" fill="#4285F4"/>' }, 269 270 // Google Sheets â green sheet with a grid. 271 sheet: { brand: true, raw: 272 '<rect x="5" y="3.5" width="14" height="17" rx="2" fill="#0F9D58"/>' + 273 '<rect x="7.5" y="8" width="9" height="8.5" rx="1" fill="#fff"/>' + 274 '<path d="M7.5 11.3h9M7.5 14h9M11 8v8.5M14 8v8.5" stroke="#0F9D58" stroke-width="1"/>' }, 275 276 // Google Calendar â white card, blue header, coloured "31". 277 calendar: { brand: true, raw: 278 '<rect x="4.5" y="5" width="15" height="15" rx="2" fill="#fff" stroke="#4285F4" stroke-width="1.3"/>' + 279 '<path d="M4.5 8.5h15V7a2 2 0 0 0-2-2h-11a2 2 0 0 0-2 2z" fill="#4285F4"/>' + 280 '<path d="M9 12.5h2v5H9zM13 12.5h2v5h-2z" fill="#4285F4" opacity=".75"/>' }, 281 282 // Google Drive â the tri-colour triangle. 283 drive: { brand: true, raw: 284 '<path d="M9.3 4 3.4 14.4l3 5.1 5.9-10.3z" fill="#0066DA"/>' + 285 '<path d="M14.7 4H9.3l5.9 10.3h5.8z" fill="#00AC47"/>' + 286 '<path d="m6.4 19.5 3-5.2h11.6l-3 5.2z" fill="#FFBA00"/>' + 287 '<path d="m12.3 9.2 2.9 5.1h-5.9z" fill="#00832D"/>' }, 288 289 // Airtable â the three-colour bars. 290 airtable: { brand: true, raw: 291 '<path d="M11.4 4.2 4 7.1c-.6.2-.6 1 0 1.3l7.4 2.9c.4.2.8.2 1.2 0l7.4-2.9c.6-.3.6-1 0-1.3l-7.4-2.9a1.6 1.6 0 0 0-1.2 0z" fill="#FFBF00"/>' + 292 '<path d="M12.7 12.4v6.9c0 .5.5.8.9.6l6.7-2.6c.3-.1.4-.4.4-.6v-6.6z" fill="#26B5F8"/>' + 293 '<path d="M11.3 12.4 4.2 15.3c-.3.1-.3.4 0 .5l2.4 1.1 4.7-1.8z" fill="#ED3049" opacity=".9"/>' + 294 '<path d="m11.3 12.4-7.1 2.9 7.1-2.9z" fill="#ED3049"/>' }, 295 296 // Notion â the page mark. 297 notion: { brand: true, raw: 298 '<rect x="4.5" y="4" width="15" height="16" rx="2" fill="#fff" stroke="#111" stroke-width="1.4"/>' + 299 '<path d="M9 16V9.2l6 6.8V9" fill="none" stroke="#111" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>' }, 300 301 // Telegram â paper-plane in Telegram blue. 302 telegram: { brand: true, raw: 303 '<circle cx="12" cy="12" r="9" fill="#29A9EB"/>' + 304 '<path d="M6 11.8 17 7.5c.5-.2 1 .1.8.7l-1.9 8.9c-.1.6-.5.7-1 .4l-2.7-2-1.3 1.3c-.2.2-.4.2-.6 0l.3-2.9 5.1-4.6c.2-.2-.05-.3-.3-.15l-6.3 4-2.7-.85c-.6-.2-.6-.6.1-.9z" fill="#fff"/>' }, 305 306 // Discord â the blurple controller-face. 307 discord: { brand: true, raw: 308 '<path d="M18.5 6.2A14 14 0 0 0 15 5l-.3.6a11 11 0 0 1 3 1.2 11 11 0 0 0-9.4 0 11 11 0 0 1 3-1.2L11 5a14 14 0 0 0-3.5 1.2C5 9.8 4.4 13.3 4.7 16.7a14 14 0 0 0 4.2 2l.9-1.4a8 8 0 0 1-1.4-.7l.4-.3a9.5 9.5 0 0 0 8.4 0l.4.3a8 8 0 0 1-1.4.7l.9 1.4a14 14 0 0 0 4.2-2c.4-3.9-.6-7.4-3.3-10.5z" fill="#5865F2"/>' + 309 '<circle cx="9.5" cy="13" r="1.3" fill="#fff"/><circle cx="14.5" cy="13" r="1.3" fill="#fff"/>' }, 310 311 // Stripe â wordmark "S" on the brand purple tile. 312 stripe: { brand: true, raw: 313 '<rect x="3.5" y="4.5" width="17" height="15" rx="3" fill="#635BFF"/>' + 314 '<path d="M11.7 10.3c0-.5.4-.7 1-.7.9 0 2 .3 2.9.8V8a7 7 0 0 0-2.9-.6c-2.3 0-3.9 1.2-3.9 3.2 0 3.1 4.2 2.6 4.2 4 0 .5-.5.7-1.1.7-1 0-2.3-.4-3.3-1v2.3c1.1.5 2.2.7 3.3.7 2.4 0 4-1.2 4-3.2 0-3.4-4.2-2.8-4.2-4.1z" fill="#fff"/>' }, 315 316 // Shopify â the green bag. 317 shopify: { brand: true, raw: 318 '<path d="M14.1 6.1c-.1-.1-.2-.1-.3-.1l-1 .1c0-.1-.5-1.6-.6-1.7-.1-.1-.3-.1-.3-.1l-.5.2c-.3-.8-.8-1.2-1.5-1.2-.5 0-1 .4-1.4 1-.6.2-1 .3-1 .4-.3.1-.3.1-.4.4-.1.2-1.7 13-1.7 13L13 19.5l3.8-.9S14.2 6.2 14.1 6.1zM10.7 5.5l-1.4.4c.3-1 .8-1.5 1.2-1.7.2.4.2 1 .2 1.3zm-1-1.8c.1 0 .2 0 .3.1-.5.2-1 .8-1.2 2l-1.1.3c.3-1.2 1.1-2.5 2-2.5z" fill="#95BF47"/>' + 319 '<path d="M13.8 6c-.1 0-.7.2-.7.2l-2.2.7s-.8-.8-.9-.9c0 0-.05-.05-.1-.05L9 19.5l3.8-.9S14.2 6.2 14.1 6.1c-.1-.1-.2-.1-.3-.1z" fill="#5E8E3E"/>' + 320 '<path d="M12.3 8.5 11.9 10s-.5-.3-1.2-.3c-1 0-1 .6-1 .8 0 .9 2.4 1.2 2.4 3.4 0 1.7-1.1 2.8-2.5 2.8-1.8 0-2.6-1.1-2.6-1.1l.5-1.6s.9.8 1.7.8c.5 0 .7-.4.7-.7 0-1.2-2-1.2-2-3.2 0-1.7 1.2-3.3 3.6-3.3.9 0 1.3.4 1.3.4z" fill="#fff"/>' }, 321 322 // HubSpot â the orange sprocket. 323 hubspot: { brand: true, raw: 324 '<path d="M16.5 9.2V6.9a1.8 1.8 0 1 0-1.4 0v2.3a5 5 0 0 0-2.3 1l
324-6-4.3a2 2 0 1 0-.9 1.3l5.9 4.2a4.9 4.9 0 0 0 .1 5.3l-1.8 1.8a1.6 1.6 0 1 0 1 1l1.8-1.8a4.9 4.9 0 1 0 3.6-8.8zm-1.4 7.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z" fill="#FF7A59"/>' }, 325 326 // Postgres â the elephant in Postgres slate-blue. 327 postgres: { brand: true, raw: 328 '<path d="M17.5 5.2c-1.2-.7-2.8-.6-4.1-.4-1-.2-2-.2-3 0-1.2-.3-2.7-.5-3.9.1C4.8 6 4.2 8.5 4.5 11c.2 1.9.9 4.6 2 6.3.5.8 1.3 1.5 2 1 .5-.4.6-1 .6-1.6.3.1.6.1.8 0l.05-.6c-.6-.15-.9-.7-1-1.3-.2-1.2.05-2.5.1-3.7 0-.6-.1-1.3.3-1.7" fill="#336791"/>' + 329 '<path d="M16.4 6.3c.4 3-.05 6.2.6 9.1.15.6.55 1.3 1.2 1.2.9-.15 1.3-1.5 1.5-2.5.4-2 .5-4.4 0-6.4-.3-1.2-1-2.5-2.2-2.8-.5-.1-1 0-1.1.4" fill="#336791"/>' + 330 '<circle cx="9.5" cy="10" r="1" fill="#fff"/><circle cx="9.5" cy="10" r=".45" fill="#336791"/>' + 331 '<path d="M11.8 9.4c.7-.2 1.5-.2 2.2 0" stroke="#fff" stroke-width=".8" stroke-linecap="round" fill="none"/>' }, 332 333 // Twilio â the red rounded tile with two dots in an arc. 334 twilio: { brand: true, raw: 335 '<rect x="3.5" y="3.5" width="17" height="17" rx="4.5" fill="#F22F46"/>' + 336 '<circle cx="9.4" cy="9.4" r="1.7" fill="#fff"/><circle cx="14.6" cy="9.4" r="1.7" fill="#fff"/>' + 337 '<circle cx="9.4" cy="14.6" r="1.7" fill="#fff"/><circle cx="14.6" cy="14.6" r="1.7" fill="#fff"/>' }, 338 339 // GitHub â the Octocat silhouette (tints via currentColor: black on light, 340 // white on dark) so it reads on both themes. 341 github: { brand: true, raw: 342 '<path fill="currentColor" d="M12 3.2a8.8 8.8 0 0 0-2.8 17.2c.45.1.6-.2.6-.43v-1.7c-2.45.5-2.97-1.05-2.97-1.05-.4-1-.98-1.3-.98-1.3-.8-.54.06-.53.06-.53.88.06 1.35.9 1.35.9.78 1.35 2.06 1 2.56.75.08-.57.3-.96.55-1.18-1.95-.22-4-1-4-4.35 0-.96.34-1.74.9-2.36-.1-.22-.4-1.12.08-2.33 0 0 .74-.24 2.42.9a8.4 8.4 0 0 1 4.4 0c1.68-1.14 2.42-.9 2.42-.9.48 1.2.18 2.1.09 2.33.56.62.9 1.4.9 2.36 0 3.36-2.05 4.12-4 4.34.32.27.6.8.6 1.62v2.4c0 .24.16.52.62.43A8.8 8.8 0 0 0 12 3.2z"/>' }, 343 344 // Jira â the blue chevron-stack mark. 345 jira: { brand: true, raw: 346 '<path d="M11.6 3 4.3 10.3a.6.6 0 0 0 0 .85l3.4 3.4 3.9-3.9a.55.55 0 0 0 0-.78z" fill="#2684FF"/>' + 347 '<path d="M11.6 7.1a3.7 3.7 0 0 0 0 5.2l1.6 1.6-3.9 3.9 2.3 2.3a.6.6 0 0 0 .85 0l5.4-5.4z" fill="#2684FF" opacity=".7"/>' }, 348 349 // Microsoft â the four-square logo. 350 microsoft: { brand: true, raw: 351 '<rect x="4" y="4" width="7" height="7" fill="#F25022"/>' + 352 '<rect x="13" y="4" width="7" height="7" fill="#7FBA00"/>' + 353 '<rect x="4" y="13" width="7" height="7" fill="#00A4EF"/>' + 354 '<rect x="13" y="13" width="7" height="7" fill="#FFB900"/>' }, 355 356 // VirusTotal â the blue shield with a clean white check (file/URL/IP verdict). 357 virustotal: { brand: true, raw: 358 '<path d="M12 3.2 5 5.7v5.1c0 4.4 3 7.9 7 9.3 4-1.4 7-4.9 7-9.3V5.7z" fill="#394EFF"/>' + 359 '<path d="M8.6 12.1 11 14.5l4.6-4.8" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' }, 360 361 // Shodan â the red tile with a white search-cursor/terminal block. 362 shodan: { brand: true, raw: 363 '<rect x="3.5" y="3.5" width="17" height="17" rx="3.5" fill="#E51111"/>' + 364 '<circle cx="11" cy="11" r="4" fill="none" stroke="#fff" stroke-width="1.8"/>' + 365 '<path d="m14.2 14.2 3 3" stroke="#fff" stroke-width="1.9" stroke-linecap="round"/>' + 366 '<circle cx="11" cy="11" r="1.4" fill="#fff"/>' }, 367 368 // ââ Connector brand marks âââââââââââââââââââââââââââââââââââââââââââââââââââ 369 // Salesforce â the blue cloud. 370 salesforce: { brand: true, raw: 371 '<path d="M10.5 7.2a3.3 3.3 0 0 1 5.8 1 3.6 3.6 0 0 1 1.4-.3 3.4 3.4 0 0 1 .5 6.7 3 3 0 0 1-3 2.4 3 3 0 0 1-1.2-.25 3.5 3.5 0 0 1-6.3-.4 3 3 0 0 1-.9.13 3.1 3.1 0 0 1-1-6 3.6 3.6 0 0 1 4.7-3.05z" fill="#00A1E0"/>' }, 372 // MongoDB â the green leaf. 373 mongodb: { brand: true, raw: 374 '<path d="M12 3.2c1.4 2.2 4.2 4.1 4.2 8.2 0 3.8-2.4 5.9-3.6 6.7l-.6 2.7-.6-2.7c-1.2-.8-3.6-2.9-3.6-6.7 0-4.1 2.8-6 4.2-8.2z" fill="#00684A"/>' + 375 '<path d="M12 3.6c1.3 2.1 3.8 3.9 3.8 7.8 0 3.3-1.9 5.2-3.2 6.2z" fill="#13AA52"/>' }, 376 // MySQL â the teal dolphin tile with a wordmark hint. 377 mysql: { brand: true, raw: 378 '<rect x="3.5" y="4.5" width="17" height="15" rx="3" fill="#00758F"/>' + 379 '<path d="M5.5 14.5c2.5 0 4-1 5.2-2.4 1-1.2 1.4-2.6 2.4-2.6.7 0 .9.4 1.2 1 .4.9.6 2 1.6 2.6.7.4 1.6.5 2.6.5" fill="none" stroke="#fff" stroke-width="1.3" stroke-linecap="round"/>' + 380 '<circle cx="16.4" cy="11.4" r=".7" fill="#fff"/>' + 381 '<path d="M17 13.6c.6.3 1.2.4 1.9.4" stroke="#F29111" stroke-width="1.2" stroke-linecap="round" fill="none"/>' }, 382 // AWS S3 â the orange bucket. 383 s3: { brand: true, raw: 384 '<path d="M5.5 6.5h13l-1 12.2a1 1 0 0 1-1 .9H7.5a1 1 0 0 1-1-.9z" fill="#E25444"/>' + 385 '<path d="M5 5.5h14v1.6a.6.6 0 0 1-.6.6H5.6a.6.6 0 0 1-.6-.
3856z" fill="#B0301E"/>' + 386 '<path d="M9.5 10.5l1.2 6M14.5 10.5l-1.2 6" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/>' }, 387 // monday.com â the three coloured dots in a row. 388 monday: { brand: true, raw: 389 '<circle cx="6.5" cy="12" r="2.6" fill="#FF3D57"/>' + 390 '<circle cx="12" cy="12" r="2.6" fill="#FFCB00"/>' + 391 '<circle cx="17.5" cy="12" r="2.6" fill="#00CA72"/>' }, 392 // Mailchimp â Freddie the mailman, simplified to the yellow head + cap. 393 mailchimp: { brand: true, raw: 394 '<circle cx="12" cy="12" r="8.5" fill="#FFE01B"/>' + 395 '<circle cx="9.4" cy="11" r="1.3" fill="#241C15"/><circle cx="14.6" cy="11" r="1.3" fill="#241C15"/>' + 396 '<path d="M9 15c1.8 1.4 4.2 1.4 6 0" stroke="#241C15" stroke-width="1.4" stroke-linecap="round" fill="none"/>' + 397 '<path d="M7 7.5c1-1.2 3-1.2 4 0M13 7.5c1-1.2 3-1.2 4 0" stroke="#241C15" stroke-width="1.2" stroke-linecap="round" fill="none"/>' }, 398 // Microsoft Teams â the purple rounded tile with a white "T". 399 teams: { brand: true, raw: 400 '<rect x="3.5" y="4.5" width="17" height="15" rx="3.5" fill="#5059C9"/>' + 401 '<circle cx="16" cy="8" r="2.2" fill="#7B83EB"/>' + 402 '<path d="M7 9.5h7v1.8h-2.6V17h-1.8v-5.7H7z" fill="#fff"/>' }, 403 // Microsoft Outlook â the blue tile with a white envelope "O". 404 outlook: { brand: true, raw: 405 '<rect x="9" y="5" width="11" height="14" rx="1.5" fill="#fff" stroke="#0A2767" stroke-width="1.1"/>' + 406 '<path d="M9 8.5 14.5 12 20 8.5" fill="none" stroke="#0364B8" stroke-width="1.3"/>' + 407 '<rect x="3" y="7" width="9" height="10" rx="2" fill="#0A78D4"/>' + 408 '<ellipse cx="7.5" cy="12" rx="2.4" ry="2.8" fill="none" stroke="#fff" stroke-width="1.5"/>' }, 409 // Trello â the blue tile with two list columns. 410 trello: { brand: true, raw: 411 '<rect x="3.5" y="3.5" width="17" height="17" rx="3.5" fill="#0079BF"/>' + 412 '<rect x="6" y="6" width="5" height="9" rx="1" fill="#fff"/>' + 413 '<rect x="13" y="6" width="5" height="5.5" rx="1" fill="#fff"/>' }, 414 // Asana â the three-dot constellation in coral. 415 asana: { brand: true, raw: 416 '<circle cx="12" cy="6.5" r="2.7" fill="#F06A6A"/>' + 417 '<circle cx="6.8" cy="15" r="2.7" fill="#F06A6A"/>' + 418 '<circle cx="17.2" cy="15" r="2.7" fill="#F06A6A"/>' }, 419 // Linear â the indigo glyph (angled square corner mark). 420 linear: { brand: true, raw: 421 '<path d="M3.6 13.2 10.8 20.4a8.8 8.8 0 0 1-7.2-7.2z" fill="#5E6AD2"/>' + 422 '<path d="M3.4 10.6 13.4 20.6a8.9 8.9 0 0 0 2.4-.7L4.1 8.2a8.9 8.9 0 0 0-.7 2.4z" fill="#5E6AD2"/>' + 423 '<path d="M5 6.6 17.4 19a9 9 0 0 0 1.6-1.4L7 5a9 9 0 0 0-2 1.6z" fill="#5E6AD2"/>' + 424 '<path d="M8.4 4 20 15.6a8.8 8.8 0 0 0-9-11.4 8.7 8.7 0 0 0-2.6.4z" fill="#5E6AD2"/>' }, 425 }; 426 427 function iconSvg(iconKey, size) { 428 var icon = ICONS[iconKey] || ICONS.box; 429 var s = size || 20; 430 var head = '<svg viewBox="0 0 24 24" width="' + s + '" height="' + s + '" ' + 431 'aria-hidden="true" focusable="false" class="au-ico' + (icon.brand ? ' au-ico-brand' : '') + '">'; 432 if (icon.brand) { 433 // Brand marks bring their own fills/colours â emit verbatim. 434 return head + icon.raw + '</svg>'; 435 } 436 // Tinted stroke glyph. 437 return head.replace('>', 438 ' fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">') + 439 icon.d + '</svg>'; 440 } 441 442 function get(key) { 443 return SERVICES[key] || SERVICES.generic; 444 } 445 446 function list() { 447 return Object.keys(SERVICES).map(function (key) { 448 var s = SERVICES[key]; 449 return { 450 key: key, label: s.label, icon: s.icon, defaultType: s.defaultType, 451 subRole: s.subRole || null, 452 category: s.category || null, 453 nodeType: s.nodeType || null, 454 }; 455 }); 456 } 457 458 // Canvas helpers for the new node kinds. A service is a note when its mirror 459 // declares the 'note' node type (defaultType:'note'); a loop is the 'loop' 460 // service / the 'loop' node type. Both are derivable from the entry alone so 461 // the canvas never has to special-case keys. 462 function isNoteService(key) { 463 var s = SERVICES[key]; 464 return !!s && (s.nodeType === 'note' || s.defaultType === 'note'); 465 } 466 function isLoopService(key) { 467 var s = SERVICES[key]; 468 return !!s && (s.nodeType === 'loop' || key === 'loop'); 469 }
470 // True when a canonical node.type is a non-executed annotation. 471 function isNoteType(type) { 472 return type === 'note'; 473 } 474 475 function colorForType(type) { 476 return (CATEGORY_META[type] || CATEGORY_META.action).color; 477 } 478 479 // ââ AI sub-node helpers (mirror the backend) ââââââââââââââââââââââââââââââââ 480 function isAiPort(port) { 481 return !!AI_PORT_SET[port]; 482 } 483 484 // Sub-node role of a service ('languageModel' | 'memory' | 'tool' | 485 // 'outputParser' | 'embedding'), or null for an ordinary main-flow service. 486 function subRoleOf(key) { 487 var svc = SERVICES[key]; 488 return (svc && svc.subRole) || null; 489 } 490 491 // The ai_* port a service attaches through, or null if it's main-flow. 492 function portForService(key) { 493 var role = subRoleOf(key); 494 return role ? (SUBROLE_TO_PORT[role] || null) : null; 495 } 496 497 // Human caption for a port ('ai_languageModel' -> 'Chat Model'). 498 function portLabel(port) { 499 return PORT_LABELS[port] || ''; 500 } 501 502 var CATALOG = { 503 services: SERVICES, 504 CATEGORY_META: CATEGORY_META, 505 AI_PORTS: AI_PORTS, 506 SUBROLE_TO_PORT: SUBROLE_TO_PORT, 507 PORT_LABELS: PORT_LABELS, 508 iconSvg: iconSvg, 509 get: get, 510 list: list, 511 colorForType: colorForType, 512 isAiPort: isAiPort, 513 subRoleOf: subRoleOf, 514 portForService: portForService, 515 portLabel: portLabel, 516 isNoteService: isNoteService, 517 isLoopService: isLoopService, 518 isNoteType: isNoteType, 519 }; 520 521 // Browser (classic script) / node+vitest (ESM side-effect import) â same file. 522 if (typeof window !== 'undefined') window.DP_AUTO_CATALOG = CATALOG; 523 if (typeof globalThis !== 'undefined') globalThis.DP_AUTO_CATALOG = CATALOG; 524})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.