@velaro/cli 1.2.0 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +161 -138
  2. package/bin/velaro.js +177 -62
  3. package/lib/api.js +91 -52
  4. package/lib/api.test.js +46 -0
  5. package/lib/banner.js +76 -0
  6. package/lib/commands/activity.js +133 -0
  7. package/lib/commands/acuity.js +66 -0
  8. package/lib/commands/agent.js +204 -50
  9. package/lib/commands/ai-config.js +193 -0
  10. package/lib/commands/ai-models.js +159 -0
  11. package/lib/commands/appointments.js +198 -0
  12. package/lib/commands/article.js +668 -388
  13. package/lib/commands/automation-draft.js +134 -0
  14. package/lib/commands/avatar.js +75 -0
  15. package/lib/commands/bigcommerce.js +50 -0
  16. package/lib/commands/billing-contacts.js +62 -0
  17. package/lib/commands/billing-email-preference.js +64 -0
  18. package/lib/commands/billing-subscription.js +265 -0
  19. package/lib/commands/billing.js +138 -0
  20. package/lib/commands/bot.js +141 -137
  21. package/lib/commands/bundle.js +168 -0
  22. package/lib/commands/calendly.js +62 -0
  23. package/lib/commands/callback.js +125 -0
  24. package/lib/commands/callrail.js +88 -0
  25. package/lib/commands/campaigns.js +44 -0
  26. package/lib/commands/case.js +102 -0
  27. package/lib/commands/check.js +163 -163
  28. package/lib/commands/compliance.js +229 -0
  29. package/lib/commands/conversation-efficiency.js +178 -0
  30. package/lib/commands/copilotstudio.js +114 -0
  31. package/lib/commands/coupon-grant.js +192 -0
  32. package/lib/commands/db.js +101 -0
  33. package/lib/commands/deployment.js +107 -107
  34. package/lib/commands/diagnostics.js +298 -0
  35. package/lib/commands/email-campaign.js +47 -0
  36. package/lib/commands/email-inbox.js +88 -0
  37. package/lib/commands/entitlement.js +176 -0
  38. package/lib/commands/env.js +45 -45
  39. package/lib/commands/feature-discovery.js +40 -0
  40. package/lib/commands/focus.js +278 -0
  41. package/lib/commands/index.js +38 -5
  42. package/lib/commands/ingest.js +31 -31
  43. package/lib/commands/inline-widget-config.js +126 -0
  44. package/lib/commands/integration.js +93 -0
  45. package/lib/commands/kb.js +450 -309
  46. package/lib/commands/login.js +86 -86
  47. package/lib/commands/logs.js +680 -0
  48. package/lib/commands/magento.js +210 -0
  49. package/lib/commands/mcp-key.js +188 -159
  50. package/lib/commands/migrate.js +134 -0
  51. package/lib/commands/migration-status.js +66 -0
  52. package/lib/commands/monday.js +137 -0
  53. package/lib/commands/netsuite.js +87 -0
  54. package/lib/commands/notifications.js +63 -0
  55. package/lib/commands/notion.js +70 -0
  56. package/lib/commands/ops.js +267 -173
  57. package/lib/commands/payment-recovery.js +170 -0
  58. package/lib/commands/pickup.js +172 -0
  59. package/lib/commands/pricing.js +132 -0
  60. package/lib/commands/product.js +55 -0
  61. package/lib/commands/recruiting.js +374 -0
  62. package/lib/commands/report.js +462 -0
  63. package/lib/commands/routing.js +304 -0
  64. package/lib/commands/rule.js +85 -85
  65. package/lib/commands/sharepoint.js +167 -0
  66. package/lib/commands/site-provision.js +68 -0
  67. package/lib/commands/site.js +62 -62
  68. package/lib/commands/sitesync.js +158 -0
  69. package/lib/commands/slack.js +64 -0
  70. package/lib/commands/squarespace.js +108 -0
  71. package/lib/commands/status.js +24 -24
  72. package/lib/commands/subscription.js +43 -0
  73. package/lib/commands/support.js +128 -0
  74. package/lib/commands/survey.js +216 -0
  75. package/lib/commands/team.js +144 -144
  76. package/lib/commands/teams-phone.js +131 -0
  77. package/lib/commands/teams.js +106 -0
  78. package/lib/commands/telephony.js +99 -0
  79. package/lib/commands/update.js +47 -47
  80. package/lib/commands/webflow.js +128 -0
  81. package/lib/commands/whoami.js +25 -22
  82. package/lib/commands/widget-container.js +152 -0
  83. package/lib/commands/woocommerce.js +240 -0
  84. package/lib/commands/workflow.js +233 -98
  85. package/lib/config.js +85 -83
  86. package/lib/kb-screenshot.js +320 -0
  87. package/lib/migrations/amscro.json +72 -0
  88. package/lib/migrations/azenta.json +68 -0
  89. package/lib/migrations/bluefire.json +49 -0
  90. package/lib/migrations/donaldson.json +75 -0
  91. package/lib/oauth.js +149 -135
  92. package/lib/run.js +21 -16
  93. package/lib/sharepoint-auth.js +138 -0
  94. package/lib/subscription.js +41 -39
  95. package/lib/track.js +35 -35
  96. package/lib/update-check.js +64 -64
  97. package/package.json +34 -19
  98. package/scripts/postinstall.js +12 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * kb-screenshot.js — capture a live staging screenshot and drop it into a KB article.
3
+ *
4
+ * Reuses:
5
+ * - the login-form-fill steps from e2e/tests/auth.setup.ts (adapted to run standalone,
6
+ * outside the Playwright test-runner lifecycle)
7
+ * - scripts/polish-screenshot.py for rounded corners / shadow / annotations
8
+ * - cli/lib/commands/article.js's adminRequest() for auth + KB article get/update
9
+ * - server/Velaro.Admin/Controllers/KB/KBArticlesController.cs's upload-image endpoint
10
+ *
11
+ * playwright is an OPTIONAL dependency of @velaro/cli (it pulls down a full Chromium
12
+ * binary) — it's dynamically imported here so a plain `velaro` install never pays that
13
+ * cost unless this command is actually used.
14
+ */
15
+
16
+ import { readFileSync, unlinkSync, existsSync } from 'fs';
17
+ import { tmpdir } from 'os';
18
+ import { join, extname, basename, dirname } from 'path';
19
+ import { fileURLToPath } from 'url';
20
+ import { execFile } from 'child_process';
21
+ import { promisify } from 'util';
22
+ import { getCredentials } from './api.js';
23
+ import { adminRequest } from './commands/article.js';
24
+
25
+ const execFileAsync = promisify(execFile);
26
+
27
+ // Resolve relative to this file's location (cli/lib/), not process.cwd() — the CLI
28
+ // is invoked from wherever the caller happens to be (cli/, repo root, or a global
29
+ // install), and process.cwd()-based resolution silently 404s outside the repo root.
30
+ // Repo layout: <repoRoot>/scripts/polish-screenshot.py, this file at <repoRoot>/cli/lib/.
31
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
32
+
33
+ const APP_URLS = {
34
+ admin: process.env.ADMIN_UI_URL || 'https://admin-staging.velaro.com',
35
+ messaging: process.env.MSG_UI_URL || 'https://messaging-staging.velaro.com',
36
+ };
37
+
38
+ // Key Vault fallback for credentials — used only when neither E2E_USER_* nor TEST_USER_*
39
+ // env vars are set. Configurable via env so this isn't hardcoded to one account forever.
40
+ const KV_NAME = process.env.KB_SCREENSHOT_KV_NAME || 'velaro-eastus-stg-kv';
41
+ const KV_EMAIL_SECRET = process.env.KB_SCREENSHOT_KV_EMAIL_SECRET || 'claude-test-agent-staging1032-email';
42
+ const KV_PASSWORD_SECRET = process.env.KB_SCREENSHOT_KV_PASSWORD_SECRET || 'claude-test-agent-staging1032-password';
43
+
44
+ /**
45
+ * Pulls the email/password pair from Azure Key Vault via DefaultAzureCredential.
46
+ * Locally this picks up the operator's own `az login` session automatically — no
47
+ * extra config needed. In CI (non-interactive), DefaultAzureCredential would need
48
+ * AZURE_CLIENT_ID/AZURE_TENANT_ID/AZURE_CLIENT_SECRET env vars (or a federated
49
+ * identity) to authenticate — out of scope here since this CLI command is
50
+ * operator-run today, not a CI step.
51
+ */
52
+ async function loadCredentialsFromKeyVault() {
53
+ const { SecretClient } = await import('@azure/keyvault-secrets');
54
+ const { DefaultAzureCredential } = await import('@azure/identity');
55
+
56
+ const vaultUrl = `https://${KV_NAME}.vault.azure.net`;
57
+ const client = new SecretClient(vaultUrl, new DefaultAzureCredential());
58
+
59
+ const [emailSecret, passwordSecret] = await Promise.all([
60
+ client.getSecret(KV_EMAIL_SECRET),
61
+ client.getSecret(KV_PASSWORD_SECRET),
62
+ ]);
63
+
64
+ return { email: emailSecret.value, password: passwordSecret.value };
65
+ }
66
+
67
+ async function loadPlaywright() {
68
+ try {
69
+ return await import('playwright');
70
+ } catch (err) {
71
+ throw new Error(
72
+ 'playwright is not installed. Run "npm install playwright" (or "npm install -g @velaro/cli" ' +
73
+ 'again after it is added) before using "velaro kb article screenshot". ' +
74
+ `(${err.message})`
75
+ );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Logs into the given app on staging using the same form-fill steps as
81
+ * e2e/tests/auth.setup.ts, navigates to `route`, and screenshots to `outputPath`.
82
+ */
83
+ export async function loginAndScreenshot(app, route, outputPath) {
84
+ let email = process.env.E2E_USER_EMAIL || process.env.TEST_USER_EMAIL;
85
+ let password = process.env.E2E_USER_PASSWORD || process.env.TEST_USER_PASSWORD;
86
+
87
+ if (!email || !password) {
88
+ try {
89
+ const kvCreds = await loadCredentialsFromKeyVault();
90
+ email = kvCreds.email;
91
+ password = kvCreds.password;
92
+ } catch (err) {
93
+ throw new Error(
94
+ 'E2E_USER_EMAIL/E2E_USER_PASSWORD (or TEST_USER_EMAIL/TEST_USER_PASSWORD) must be set ' +
95
+ 'to capture a live screenshot — these are the same env vars used by e2e/tests/auth.setup.ts. ' +
96
+ `Key Vault fallback also failed: ${err.message}`
97
+ );
98
+ }
99
+ }
100
+
101
+ if (!email || !password) {
102
+ throw new Error(
103
+ 'E2E_USER_EMAIL/E2E_USER_PASSWORD (or TEST_USER_EMAIL/TEST_USER_PASSWORD) must be set, ' +
104
+ `and Key Vault (${KV_NAME}) did not return usable values for secrets ` +
105
+ `"${KV_EMAIL_SECRET}"/"${KV_PASSWORD_SECRET}".`
106
+ );
107
+ }
108
+
109
+ const baseUrl = APP_URLS[app];
110
+ if (!baseUrl) throw new Error(`Unknown app "${app}" — expected "admin" or "messaging".`);
111
+
112
+ const { chromium } = await loadPlaywright();
113
+ const browser = await chromium.launch();
114
+
115
+ try {
116
+ const context = await browser.newContext();
117
+ const page = await context.newPage();
118
+
119
+ // ── Login (adapted from e2e/tests/auth.setup.ts) ──────────────────────────
120
+ // Role/label based selectors, not type-attribute selectors — the CIAM login page's
121
+ // markup does not reliably render the email field as a literal input[type="email"]
122
+ // or the submit controls as input[type="submit"] (found 2026-08-01 in auth.setup.ts,
123
+ // confirmed again 2026-08-13 against live admin-staging while verifying the Key
124
+ // Vault credential fallback below: the email step is input[name="username"]
125
+ // type="text", and both submit controls are <button type="submit">). Role/label
126
+ // selectors track the accessible page contract instead of an implementation detail
127
+ // that can change under us.
128
+ await page.goto(baseUrl);
129
+ await page.waitForURL(/login\.velaro\.com/, { timeout: 30_000 });
130
+
131
+ await page.getByRole('textbox', { name: /email/i }).fill(email);
132
+ await page.getByRole('button', { name: /next|sign in/i }).click();
133
+
134
+ await page.getByLabel(/password/i).fill(password);
135
+ await page.getByRole('button', { name: /sign in|next/i }).click();
136
+
137
+ // Handle "Stay signed in?" prompt — not always shown.
138
+ try {
139
+ await page.getByRole('button', { name: /yes|no|stay signed in/i }).first().click({ timeout: 5_000 });
140
+ } catch { /* prompt not shown — proceed */ }
141
+
142
+ await page.waitForURL(/velaro\.com/, { timeout: 30_000 });
143
+
144
+ // ── Navigate + capture ──────────────────────────────────────────────────
145
+ const target = new URL(route, baseUrl).toString();
146
+ await page.goto(target);
147
+ await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {
148
+ // Some pages keep a long-lived connection (SignalR) open — don't fail the
149
+ // capture just because the network never goes fully idle.
150
+ });
151
+
152
+ await page.screenshot({ path: outputPath, fullPage: true });
153
+ } finally {
154
+ await browser.close();
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Shells out to scripts/polish-screenshot.py. Flags match that script's argparse
160
+ * definitions exactly: `--annotate "x,y,label"`, repeatable.
161
+ */
162
+ export async function polishScreenshot(inputPath, annotations, polishOpts) {
163
+ const hasAnnotations = annotations && annotations.length > 0;
164
+ if (!hasAnnotations && !polishOpts) return inputPath;
165
+
166
+ const outputPath = inputPath.replace(/\.(png|jpg|jpeg)$/i, '.polished.png');
167
+ const scriptPath = join(REPO_ROOT, 'scripts', 'polish-screenshot.py');
168
+
169
+ const args = [scriptPath, inputPath, outputPath];
170
+ for (const a of (annotations || [])) {
171
+ if (a.x == null || a.y == null || Number.isNaN(a.x) || Number.isNaN(a.y)) {
172
+ throw new Error(`Invalid annotation: ${JSON.stringify(a)} — expected {x, y, label}`);
173
+ }
174
+ args.push('--annotate', `${a.x},${a.y},${a.label ?? ''}`);
175
+ }
176
+ // Standard KB style-guide defaults (see ~/.claude/skills/polish-screenshot/SKILL.md) — always
177
+ // route product/KB screenshots through this rather than embedding raw captures.
178
+ if (polishOpts) {
179
+ args.push('--bg', polishOpts.bg || 'F4F5F7');
180
+ args.push('--pad', String(polishOpts.pad ?? 40));
181
+ args.push('--radius', String(polishOpts.radius ?? 10));
182
+ args.push('--shadow-size', String(polishOpts.shadowSize ?? 24));
183
+ if (polishOpts.border !== false) args.push('--border');
184
+ }
185
+
186
+ try {
187
+ await execFileAsync('python', args);
188
+ } catch (err) {
189
+ throw new Error(`scripts/polish-screenshot.py failed: ${err.message}`);
190
+ }
191
+
192
+ return outputPath;
193
+ }
194
+
195
+ function mimeTypeFor(path) {
196
+ const ext = extname(path).toLowerCase();
197
+ if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
198
+ if (ext === '.gif') return 'image/gif';
199
+ if (ext === '.webp') return 'image/webp';
200
+ return 'image/png';
201
+ }
202
+
203
+ /** Uploads via the existing KBArticlesController upload-image endpoint. Returns the image URL. */
204
+ export async function uploadImage(imagePath, creds) {
205
+ const apiBase = creds.adminApiBase || 'https://help.velaro.com';
206
+
207
+ const buffer = readFileSync(imagePath);
208
+ const form = new FormData();
209
+ form.append('file', new Blob([buffer], { type: mimeTypeFor(imagePath) }), basename(imagePath));
210
+
211
+ const res = await fetch(`${apiBase}/api/kb/articles/upload-image`, {
212
+ method: 'POST',
213
+ headers: { Authorization: `Bearer ${creds.velaroToken}` },
214
+ body: form,
215
+ });
216
+
217
+ if (!res.ok) {
218
+ let msg = `upload-image -> ${res.status}`;
219
+ try { const t = await res.text(); if (t) msg += `: ${t.slice(0, 300)}`; } catch { /* status already captured */ }
220
+ throw new Error(msg);
221
+ }
222
+
223
+ const data = await res.json();
224
+ if (!data?.url) throw new Error('upload-image response did not include a url');
225
+ return data.url;
226
+ }
227
+
228
+ /**
229
+ * Case-insensitive substring match against <h2>/<h3> text content. Inserts immediately
230
+ * after the matched heading's closing tag; appends to the end if no heading (or no
231
+ * match) was given.
232
+ */
233
+ export function insertImageAfterHeading(html, heading, imgTag) {
234
+ const body = html ?? '';
235
+
236
+ if (!heading) {
237
+ return { html: `${body}\n${imgTag}`, insertedAfterHeading: false };
238
+ }
239
+
240
+ const headingRegex = /<h([23])[^>]*>([\s\S]*?)<\/h\1>/gi;
241
+ let match;
242
+ while ((match = headingRegex.exec(body)) !== null) {
243
+ const text = match[2].replace(/<[^>]+>/g, '').trim();
244
+ if (text.toLowerCase().includes(heading.toLowerCase())) {
245
+ const insertAt = match.index + match[0].length;
246
+ return {
247
+ html: `${body.slice(0, insertAt)}\n${imgTag}\n${body.slice(insertAt)}`,
248
+ insertedAfterHeading: true,
249
+ };
250
+ }
251
+ }
252
+
253
+ return { html: `${body}\n${imgTag}`, insertedAfterHeading: false };
254
+ }
255
+
256
+ export function escapeAttr(s) {
257
+ return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
258
+ }
259
+
260
+ /**
261
+ * @param {object} opts
262
+ * @param {"admin"|"messaging"} opts.app
263
+ * @param {string} opts.route
264
+ * @param {number} opts.articleId
265
+ * @param {string} [opts.heading]
266
+ * @param {{x:number,y:number,label?:string}[]} [opts.annotations]
267
+ * @param {{token:string, apiBase:string}} [opts.auth] Explicit auth override for callers
268
+ * that don't use the CLI's stored login config (e.g. mcp/server.js, which authenticates
269
+ * via VELARO_MCP_KEY/VELARO_JWT env vars). CLI usage omits this and falls back to
270
+ * getCredentials() (the normal `velaro login` flow).
271
+ * @returns {Promise<{articleId:number, imageUrl:string, insertedAfterHeading:boolean}>}
272
+ */
273
+ export async function captureKbScreenshot({ app, route, articleId, heading, annotations, auth }) {
274
+ if (!app || !APP_URLS[app]) throw new Error('app must be "admin" or "messaging"');
275
+ if (!route) throw new Error('route is required');
276
+ if (!articleId) throw new Error('articleId is required');
277
+
278
+ const rawPath = join(tmpdir(), `kb-screenshot-${articleId}-${Date.now()}.png`);
279
+ let polishedPath = rawPath;
280
+
281
+ try {
282
+ const creds = auth
283
+ ? { velaroToken: auth.token, adminApiBase: auth.apiBase }
284
+ : await getCredentials();
285
+
286
+ await loginAndScreenshot(app, route, rawPath);
287
+ polishedPath = await polishScreenshot(rawPath, annotations);
288
+ const imageUrl = await uploadImage(polishedPath, creds);
289
+
290
+ const article = await adminRequest('GET', `/api/kb/articles?id=${articleId}`, undefined, creds);
291
+ if (!article) throw new Error(`Article ${articleId} not found`);
292
+
293
+ const imgTag = `<img src="${imageUrl}" alt="${heading ? escapeAttr(heading) : 'Screenshot'}" />`;
294
+ const { html: newContent, insertedAfterHeading } = insertImageAfterHeading(article.content, heading, imgTag);
295
+
296
+ await adminRequest('PUT', `/api/kb/articles?id=${articleId}`, {
297
+ title: article.title,
298
+ slug: article.slug,
299
+ topicId: article.topicId,
300
+ description: article.description,
301
+ content: newContent,
302
+ visibility: article.visibility,
303
+ searchTags: article.searchTags,
304
+ isPublished: article.isPublished,
305
+ displayPriority: article.displayPriority,
306
+ enableBotResponse: article.enableBotResponse,
307
+ botSummary: article.botSummary,
308
+ isFeatured: article.isFeatured,
309
+ }, creds);
310
+
311
+ return { articleId, imageUrl, insertedAfterHeading };
312
+ } catch (err) {
313
+ throw new Error(`kb screenshot capture failed: ${err.message}`);
314
+ } finally {
315
+ // Best-effort cleanup — never let a temp-file error mask the real result/error.
316
+ for (const p of new Set([rawPath, polishedPath])) {
317
+ try { if (existsSync(p)) unlinkSync(p); } catch { /* ignore cleanup failure */ }
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "Amscro / Oxygen Concentrator",
3
+ "slug": "amscro",
4
+ "description": "Healthcare DME: new/existing patient intake routing, widget hidden on insurance pages, ServiceNow post-chat.",
5
+ "patterns": ["healthcare", "prechat-intake", "url-routing", "widget-hide", "post-chat-crm"],
6
+ "notes": [
7
+ "HIPAA-adjacent — no PHI in chat. Conversations are sensitive.",
8
+ "Widget hidden on /insurance/* — those patients use the phone",
9
+ "Pre-chat form asks 'New patient or existing patient?' — feeds routing conditions",
10
+ "ServiceNow integration required for post-chat ticket creation (CSAT 1-2 stars)"
11
+ ],
12
+ "resolve": {
13
+ "ai_configs": {
14
+ "INTAKE_BOT": "New Patient Intake Bot"
15
+ },
16
+ "teams": {
17
+ "INTAKE_TEAM": "Intake",
18
+ "PATIENT_SUPPORT_TEAM": "Patient Support",
19
+ "SERVICE_TEAM": "Equipment Service",
20
+ "GENERAL_TEAM": "General Support"
21
+ }
22
+ },
23
+ "routing_rules": [
24
+ {
25
+ "name": "New Patient — Intake Bot",
26
+ "trigger": "use-ai",
27
+ "priority": 100,
28
+ "expression": [[{"type": "surveyAnswer", "data": "{\"answer\":\"new\",\"comparator\":\"equals\"}"}]],
29
+ "value": {"aiConfig": "@INTAKE_BOT"},
30
+ "notes": "Condition fires when pre-chat survey answer = 'new'. Adjust surveyAnswer key to match your actual pre-chat survey field."
31
+ },
32
+ {
33
+ "name": "Existing Patient Support",
34
+ "trigger": "route-to-team",
35
+ "priority": 200,
36
+ "expression": [[{"type": "surveyAnswer", "data": "{\"answer\":\"existing\",\"comparator\":\"equals\"}"}]],
37
+ "value": {"teamId": "@PATIENT_SUPPORT_TEAM"},
38
+ "notes": "Condition fires when pre-chat survey answer = 'existing'."
39
+ },
40
+ {
41
+ "name": "Equipment Service",
42
+ "trigger": "route-to-team",
43
+ "priority": 300,
44
+ "expression": [[{"type": "url", "data": "{\"url\":\"/equipment\",\"comparator\":\"contains\"}"}]],
45
+ "value": {"teamId": "@SERVICE_TEAM"}
46
+ },
47
+ {
48
+ "name": "Default — General Support",
49
+ "trigger": "route-to-team",
50
+ "priority": 9999,
51
+ "expression": [],
52
+ "value": {"teamId": "@GENERAL_TEAM"}
53
+ }
54
+ ],
55
+ "workflows": [
56
+ {
57
+ "name": "Missed Chat Alert — Unresponsive Visitor (5 min)",
58
+ "trigger": "unresponsive_visitor",
59
+ "notes": "Configure email-to-supervisor action after creation. Set timeout to 5 minutes."
60
+ },
61
+ {
62
+ "name": "CSAT Recovery — ServiceNow Ticket",
63
+ "trigger": "csat_submitted",
64
+ "notes": "Filter to 1-2 star ratings. Create ServiceNow incident. Wire ServiceNow integration credentials first."
65
+ },
66
+ {
67
+ "name": "Widget — Hide on Insurance Portal",
68
+ "trigger": "widget_loaded",
69
+ "notes": "Add URL condition (/insurance/*) and Hide Widget action. This is the widget_loaded workflow that suppresses chat on insurance pages."
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "Azenta Life Sciences",
3
+ "slug": "azenta",
4
+ "description": "Multi-department life sciences: Genewiz, Sample Management, Storage. URL-based routing, SLA alerts, LIMS post-chat push.",
5
+ "patterns": ["multi-department", "url-routing", "sla-alert", "post-chat-crm"],
6
+ "notes": [
7
+ "Victoria is the technical contact — she handles the API side (LIMS config)",
8
+ "Lisa Hague owns analytics and sentiment reporting",
9
+ "LIMS: clims4.genewiz.com — no public REST, needs IT credentials for integration",
10
+ "Phase 1: read-only LIMS lookup and how-to flows, web chat only",
11
+ "AI config names in this profile are suggestions — match to your actual bot names"
12
+ ],
13
+ "resolve": {
14
+ "ai_configs": {
15
+ "GENEWIZ_BOT": "Genewiz Sales Bot",
16
+ "SAMPLE_BOT": "Sample Support Bot",
17
+ "TRIAGE_BOT": "AI Triage Bot"
18
+ },
19
+ "teams": {
20
+ "GENEWIZ_TEAM": "Genewiz Sales",
21
+ "SAMPLE_TEAM": "Sample Management",
22
+ "STORAGE_TEAM": "Storage",
23
+ "SKILLS_TEAM": "Skills"
24
+ }
25
+ },
26
+ "routing_rules": [
27
+ {
28
+ "name": "Genewiz Orders — AI Bot",
29
+ "trigger": "use-ai",
30
+ "priority": 100,
31
+ "expression": [[{"type": "url", "data": "{\"url\":\"/genewiz/\",\"comparator\":\"contains\"}"}]],
32
+ "value": {"aiConfig": "@GENEWIZ_BOT"}
33
+ },
34
+ {
35
+ "name": "Sample Management — AI Bot",
36
+ "trigger": "use-ai",
37
+ "priority": 200,
38
+ "expression": [[{"type": "url", "data": "{\"url\":\"/samples/\",\"comparator\":\"contains\"}"}]],
39
+ "value": {"aiConfig": "@SAMPLE_BOT"}
40
+ },
41
+ {
42
+ "name": "Storage — Direct Team",
43
+ "trigger": "route-to-team",
44
+ "priority": 300,
45
+ "expression": [[{"type": "url", "data": "{\"url\":\"/storage/\",\"comparator\":\"contains\"}"}]],
46
+ "value": {"teamId": "@STORAGE_TEAM"}
47
+ },
48
+ {
49
+ "name": "Default — AI Triage",
50
+ "trigger": "use-ai",
51
+ "priority": 9999,
52
+ "expression": [],
53
+ "value": {"aiConfig": "@TRIAGE_BOT"}
54
+ }
55
+ ],
56
+ "workflows": [
57
+ {
58
+ "name": "SLA Breach Alert — Genewiz (10 min)",
59
+ "trigger": "sla_breach",
60
+ "notes": "Configure webhook to #life-sciences-alerts Slack channel after creation"
61
+ },
62
+ {
63
+ "name": "CSAT Recovery — LIMS Push",
64
+ "trigger": "conversation_resolved",
65
+ "notes": "Post-chat CSAT + push summary to LIMS. Wire LIMS credentials in integration settings after creation."
66
+ }
67
+ ]
68
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "Bluefire Industrial Services",
3
+ "slug": "bluefire",
4
+ "description": "Front-door AI disambiguates department (Sales/Parts/Service/Billing) then routes. Single priority-100 bot handles all routing internally. Post-chat label workflow creates Salesforce tickets.",
5
+ "patterns": ["front-door-ai", "department-routing", "post-chat-crm"],
6
+ "notes": [
7
+ "Key difference from Azenta/Donaldson: ONE workflow at priority #100 does all routing via AI branching",
8
+ "The AI bot classifies intent (Sales/Parts/Service/Billing) and transfers to the right team",
9
+ "No URL conditions needed — front-door AI handles everything",
10
+ "Fallback at #9999 catches anything the AI doesn't classify",
11
+ "Salesforce integration required for the 'urgent' label workflow"
12
+ ],
13
+ "resolve": {
14
+ "ai_configs": {
15
+ "FRONT_DOOR_BOT": "Bluefire Front Door Bot"
16
+ },
17
+ "teams": {
18
+ "SALES_TEAM": "Sales",
19
+ "PARTS_TEAM": "Parts",
20
+ "SERVICE_TEAM": "Service",
21
+ "BILLING_TEAM": "Billing",
22
+ "DEFAULT_TEAM": "General Support"
23
+ }
24
+ },
25
+ "routing_rules": [
26
+ {
27
+ "name": "Front Door AI — All Routing",
28
+ "trigger": "use-ai",
29
+ "priority": 100,
30
+ "expression": [],
31
+ "value": {"aiConfig": "@FRONT_DOOR_BOT"},
32
+ "notes": "This single rule handles all routing. Configure the AI bot with transfer actions for Sales/Parts/Service/Billing intents."
33
+ },
34
+ {
35
+ "name": "Default Fallback",
36
+ "trigger": "route-to-team",
37
+ "priority": 9999,
38
+ "expression": [],
39
+ "value": {"teamId": "@DEFAULT_TEAM"}
40
+ }
41
+ ],
42
+ "workflows": [
43
+ {
44
+ "name": "Urgent Label — Salesforce Ticket",
45
+ "trigger": "label_applied",
46
+ "notes": "Filter to 'urgent' label. Create Salesforce case. Wire Salesforce integration first."
47
+ }
48
+ ]
49
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "Donaldson — 100+ Location Routing",
3
+ "slug": "donaldson",
4
+ "description": "Industrial filtration: 80-120 priority-ordered routing rules by URL + language. This profile creates a representative sample of the pattern — add remaining rules manually or via --file with a custom bulk export.",
5
+ "patterns": ["multi-location", "url-routing", "language-routing", "post-chat-crm"],
6
+ "notes": [
7
+ "This profile creates a SAMPLE of the Donaldson pattern (10 representative rules)",
8
+ "Full migration requires a custom JSON export from their Livefluence config",
9
+ "Use --file donaldson-full-export.json for a complete migration",
10
+ "Priority gaps (100, 110, 200...) are intentional — leave room to insert rules between regions",
11
+ "Salesforce integration required for post-chat case sync",
12
+ "Bot handoff uses confidence threshold — configure in the AI config settings"
13
+ ],
14
+ "resolve": {
15
+ "ai_configs": {
16
+ "BOT_EN": "Donaldson English Bot",
17
+ "BOT_DE": "Donaldson German Bot",
18
+ "BOT_FR": "Donaldson French Bot",
19
+ "BOT_JP": "Donaldson Japan Bot",
20
+ "BOT_DEFAULT": "Donaldson Default Bot"
21
+ },
22
+ "teams": {
23
+ "EMEA_DE_TEAM": "EMEA German",
24
+ "EMEA_FR_TEAM": "EMEA French",
25
+ "APAC_JP_TEAM": "APAC Japan",
26
+ "HE_TEAM": "Heavy Equipment Sales",
27
+ "GLOBAL_TEAM": "Global Support"
28
+ }
29
+ },
30
+ "routing_rules": [
31
+ {
32
+ "name": "EMEA — German Visitors",
33
+ "trigger": "use-ai",
34
+ "priority": 100,
35
+ "expression": [[{"type": "language", "data": "{\"language\":\"de\",\"comparator\":\"equals\"}"}]],
36
+ "value": {"aiConfig": "@BOT_DE"},
37
+ "notes": "Language condition. Adjust condition type/data to match your expression format."
38
+ },
39
+ {
40
+ "name": "EMEA — French Visitors",
41
+ "trigger": "use-ai",
42
+ "priority": 110,
43
+ "expression": [[{"type": "language", "data": "{\"language\":\"fr\",\"comparator\":\"equals\"}"}]],
44
+ "value": {"aiConfig": "@BOT_FR"}
45
+ },
46
+ {
47
+ "name": "APAC — Japanese Visitors",
48
+ "trigger": "use-ai",
49
+ "priority": 200,
50
+ "expression": [[{"type": "language", "data": "{\"language\":\"ja\",\"comparator\":\"equals\"}"}]],
51
+ "value": {"aiConfig": "@BOT_JP"}
52
+ },
53
+ {
54
+ "name": "US — Heavy Equipment",
55
+ "trigger": "use-ai",
56
+ "priority": 300,
57
+ "expression": [[{"type": "url", "data": "{\"url\":\"/heavy-equipment/\",\"comparator\":\"contains\"}"}]],
58
+ "value": {"aiConfig": "@BOT_EN"}
59
+ },
60
+ {
61
+ "name": "Default — Global Support",
62
+ "trigger": "use-ai",
63
+ "priority": 9999,
64
+ "expression": [],
65
+ "value": {"aiConfig": "@BOT_DEFAULT"}
66
+ }
67
+ ],
68
+ "workflows": [
69
+ {
70
+ "name": "Post-Chat — Salesforce Case Sync",
71
+ "trigger": "conversation_resolved",
72
+ "notes": "Push resolved conversation to Salesforce case. Wire Salesforce integration first."
73
+ }
74
+ ]
75
+ }