PageSourceSearch

https://droprompt.com/_static/d855cbd3e0764220ae97ac7c/assets/automation/service-catalog.js?v=7

js droprompt.com collected 2026-10-02 01:28:55 UTC 37,349 bytes, 524 lines download raw bytes

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.