@velaro/cli 1.2.0 → 1.4.9
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.
- package/README.md +161 -138
- package/bin/velaro.js +177 -62
- package/lib/api.js +91 -52
- package/lib/api.test.js +46 -0
- package/lib/banner.js +76 -0
- package/lib/commands/activity.js +133 -0
- package/lib/commands/acuity.js +66 -0
- package/lib/commands/agent.js +204 -50
- package/lib/commands/ai-config.js +193 -0
- package/lib/commands/ai-models.js +159 -0
- package/lib/commands/appointments.js +198 -0
- package/lib/commands/article.js +668 -388
- package/lib/commands/automation-draft.js +134 -0
- package/lib/commands/avatar.js +75 -0
- package/lib/commands/bigcommerce.js +50 -0
- package/lib/commands/billing-contacts.js +62 -0
- package/lib/commands/billing-email-preference.js +64 -0
- package/lib/commands/billing-subscription.js +265 -0
- package/lib/commands/billing.js +138 -0
- package/lib/commands/bot.js +141 -137
- package/lib/commands/bundle.js +168 -0
- package/lib/commands/calendly.js +62 -0
- package/lib/commands/callback.js +125 -0
- package/lib/commands/callrail.js +88 -0
- package/lib/commands/campaigns.js +44 -0
- package/lib/commands/case.js +102 -0
- package/lib/commands/check.js +163 -163
- package/lib/commands/compliance.js +229 -0
- package/lib/commands/conversation-efficiency.js +178 -0
- package/lib/commands/copilotstudio.js +114 -0
- package/lib/commands/coupon-grant.js +192 -0
- package/lib/commands/db.js +101 -0
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/diagnostics.js +298 -0
- package/lib/commands/email-campaign.js +47 -0
- package/lib/commands/email-inbox.js +88 -0
- package/lib/commands/entitlement.js +176 -0
- package/lib/commands/env.js +45 -45
- package/lib/commands/feature-discovery.js +40 -0
- package/lib/commands/focus.js +278 -0
- package/lib/commands/index.js +38 -5
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/inline-widget-config.js +126 -0
- package/lib/commands/integration.js +93 -0
- package/lib/commands/kb.js +450 -309
- package/lib/commands/login.js +86 -86
- package/lib/commands/logs.js +680 -0
- package/lib/commands/magento.js +210 -0
- package/lib/commands/mcp-key.js +188 -159
- package/lib/commands/migrate.js +134 -0
- package/lib/commands/migration-status.js +66 -0
- package/lib/commands/monday.js +137 -0
- package/lib/commands/netsuite.js +87 -0
- package/lib/commands/notifications.js +63 -0
- package/lib/commands/notion.js +70 -0
- package/lib/commands/ops.js +267 -173
- package/lib/commands/payment-recovery.js +170 -0
- package/lib/commands/pickup.js +172 -0
- package/lib/commands/pricing.js +132 -0
- package/lib/commands/product.js +55 -0
- package/lib/commands/recruiting.js +374 -0
- package/lib/commands/report.js +462 -0
- package/lib/commands/routing.js +304 -0
- package/lib/commands/rule.js +85 -85
- package/lib/commands/sharepoint.js +167 -0
- package/lib/commands/site-provision.js +68 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/sitesync.js +158 -0
- package/lib/commands/slack.js +64 -0
- package/lib/commands/squarespace.js +108 -0
- package/lib/commands/status.js +24 -24
- package/lib/commands/subscription.js +43 -0
- package/lib/commands/support.js +128 -0
- package/lib/commands/survey.js +216 -0
- package/lib/commands/team.js +144 -144
- package/lib/commands/teams-phone.js +131 -0
- package/lib/commands/teams.js +106 -0
- package/lib/commands/telephony.js +99 -0
- package/lib/commands/update.js +47 -47
- package/lib/commands/webflow.js +128 -0
- package/lib/commands/whoami.js +25 -22
- package/lib/commands/widget-container.js +152 -0
- package/lib/commands/woocommerce.js +240 -0
- package/lib/commands/workflow.js +233 -98
- package/lib/config.js +85 -83
- package/lib/kb-screenshot.js +320 -0
- package/lib/migrations/amscro.json +72 -0
- package/lib/migrations/azenta.json +68 -0
- package/lib/migrations/bluefire.json +49 -0
- package/lib/migrations/donaldson.json +75 -0
- package/lib/oauth.js +149 -135
- package/lib/run.js +21 -16
- package/lib/sharepoint-auth.js +138 -0
- package/lib/subscription.js +41 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +34 -19
- 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, '&').replace(/"/g, '"');
|
|
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
|
+
}
|