@lead-routing/cli 0.8.3 → 0.8.4
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/dist/index.js +122 -115
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -112,38 +112,35 @@ function bail2(value) {
|
|
|
112
112
|
async function collectConfig(opts = {}, authEmail, authPassword) {
|
|
113
113
|
const crmType = opts.crmType ?? "salesforce";
|
|
114
114
|
note2(
|
|
115
|
-
"You will need:\n \u2022
|
|
115
|
+
"You will need:\n \u2022 A domain with wildcard DNS (*.acme.com) pointing to your server",
|
|
116
116
|
"Before you begin"
|
|
117
117
|
);
|
|
118
|
-
const
|
|
119
|
-
message: "
|
|
120
|
-
placeholder: "
|
|
118
|
+
const domain = await text2({
|
|
119
|
+
message: "Your domain (we'll create app/api/evals/mcp subdomains):",
|
|
120
|
+
placeholder: "acme.com",
|
|
121
121
|
validate: (v) => {
|
|
122
|
-
if (!v) return "
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (u.protocol !== "https:") return "Must be an HTTPS URL (required for Salesforce OAuth)";
|
|
126
|
-
} catch {
|
|
127
|
-
return "Must be a valid URL (e.g. https://routing.acme.com)";
|
|
128
|
-
}
|
|
122
|
+
if (!v?.trim()) return "Domain is required";
|
|
123
|
+
const clean = v.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
124
|
+
if (!clean.includes(".")) return "Enter a valid domain (e.g. acme.com)";
|
|
129
125
|
}
|
|
130
126
|
});
|
|
131
|
-
if (isCancel2(
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
127
|
+
if (isCancel2(domain)) bail2(domain);
|
|
128
|
+
const baseDomain = domain.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
129
|
+
const appUrl = `https://app.${baseDomain}`;
|
|
130
|
+
const engineUrl = `https://api.${baseDomain}`;
|
|
131
|
+
const langfuseUrl = `https://evals.${baseDomain}`;
|
|
132
|
+
const mcpUrl = `https://mcp.${baseDomain}`;
|
|
133
|
+
note2(
|
|
134
|
+
[
|
|
135
|
+
`App: ${appUrl}`,
|
|
136
|
+
`Engine: ${engineUrl}`,
|
|
137
|
+
`Evals: ${langfuseUrl}`,
|
|
138
|
+
`MCP: ${mcpUrl}`,
|
|
139
|
+
"",
|
|
140
|
+
`Add this DNS record: *.${baseDomain} -> A -> <your server IP>`
|
|
141
|
+
].join("\n"),
|
|
142
|
+
"URLs"
|
|
143
|
+
);
|
|
147
144
|
const dbPassword = generateSecret(16);
|
|
148
145
|
const managedDb = !opts.externalDb;
|
|
149
146
|
const databaseUrl = opts.externalDb ?? `postgresql://leadrouting:${dbPassword}@postgres:5432/leadrouting`;
|
|
@@ -186,8 +183,11 @@ async function collectConfig(opts = {}, authEmail, authPassword) {
|
|
|
186
183
|
const hubspotClientId = void 0;
|
|
187
184
|
const hubspotClientSecret = void 0;
|
|
188
185
|
return {
|
|
189
|
-
appUrl
|
|
190
|
-
engineUrl
|
|
186
|
+
appUrl,
|
|
187
|
+
engineUrl,
|
|
188
|
+
baseDomain,
|
|
189
|
+
langfuseUrl,
|
|
190
|
+
mcpUrl,
|
|
191
191
|
crmType,
|
|
192
192
|
managedDb,
|
|
193
193
|
databaseUrl,
|
|
@@ -294,9 +294,26 @@ function renderDockerCompose(c) {
|
|
|
294
294
|
depends_on:
|
|
295
295
|
postgres:
|
|
296
296
|
condition: service_healthy
|
|
297
|
+
` : "";
|
|
298
|
+
const mcpService = c.managedMcp ? `
|
|
299
|
+
mcp:
|
|
300
|
+
image: ghcr.io/atgatzby/lead-routing-mcp:latest
|
|
301
|
+
restart: unless-stopped
|
|
302
|
+
environment:
|
|
303
|
+
APP_URL: http://web:3000
|
|
304
|
+
ENGINE_URL: http://engine:3001
|
|
305
|
+
API_TOKEN: ${c.mcpApiToken ?? ""}
|
|
306
|
+
WEBHOOK_SECRET: ${c.mcpWebhookSecret ?? ""}
|
|
307
|
+
CRM_TYPE: ${c.mcpCrmType ?? "salesforce"}
|
|
308
|
+
PORT: "3100"
|
|
309
|
+
TRANSPORT: http
|
|
310
|
+
depends_on:
|
|
311
|
+
web:
|
|
312
|
+
condition: service_healthy
|
|
297
313
|
` : "";
|
|
298
314
|
const caddyDeps = ["web", "engine"];
|
|
299
315
|
if (c.managedLangfuse) caddyDeps.push("langfuse");
|
|
316
|
+
if (c.managedMcp) caddyDeps.push("mcp");
|
|
300
317
|
const caddyService = `
|
|
301
318
|
caddy:
|
|
302
319
|
image: caddy:2-alpine
|
|
@@ -324,6 +341,7 @@ ${caddyDeps.map((d) => ` - ${d}`).join("\n")}
|
|
|
324
341
|
webService.trimEnd(),
|
|
325
342
|
engineService.trimEnd(),
|
|
326
343
|
langfuseService.trimEnd(),
|
|
344
|
+
mcpService.trimEnd(),
|
|
327
345
|
caddyService.trimEnd(),
|
|
328
346
|
volumes
|
|
329
347
|
].filter(Boolean).join("\n");
|
|
@@ -443,68 +461,32 @@ function renderEnvEngine(c) {
|
|
|
443
461
|
}
|
|
444
462
|
|
|
445
463
|
// src/templates/caddy.ts
|
|
446
|
-
function renderCaddyfile(
|
|
447
|
-
|
|
448
|
-
|
|
464
|
+
function renderCaddyfile(appUrlOrConfig, engineUrl, langfuseUrl) {
|
|
465
|
+
let config2;
|
|
466
|
+
if (typeof appUrlOrConfig === "string") {
|
|
467
|
+
config2 = { appUrl: appUrlOrConfig, engineUrl, langfuseUrl };
|
|
468
|
+
} else {
|
|
469
|
+
config2 = appUrlOrConfig;
|
|
470
|
+
}
|
|
471
|
+
const appHost = new URL(config2.appUrl).hostname;
|
|
472
|
+
const engineParsed = new URL(config2.engineUrl);
|
|
449
473
|
const engineHost = engineParsed.hostname;
|
|
450
474
|
const enginePort = engineParsed.port;
|
|
451
475
|
let langfuseHostname = "";
|
|
452
|
-
if (langfuseUrl) {
|
|
476
|
+
if (config2.langfuseUrl) {
|
|
453
477
|
try {
|
|
454
|
-
langfuseHostname = new URL(langfuseUrl).hostname;
|
|
478
|
+
langfuseHostname = new URL(config2.langfuseUrl).hostname;
|
|
455
479
|
} catch {
|
|
456
480
|
}
|
|
457
481
|
}
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
``,
|
|
465
|
-
`(security_headers) {`,
|
|
466
|
-
` header {`,
|
|
467
|
-
` X-Content-Type-Options nosniff`,
|
|
468
|
-
` X-Frame-Options DENY`,
|
|
469
|
-
` Referrer-Policy strict-origin-when-cross-origin`,
|
|
470
|
-
` Permissions-Policy interest-cohort=()`,
|
|
471
|
-
` Strict-Transport-Security "max-age=31536000; includeSubDomains"`,
|
|
472
|
-
` }`,
|
|
473
|
-
`}`,
|
|
474
|
-
``,
|
|
475
|
-
`${appHost} {`,
|
|
476
|
-
` import security_headers`,
|
|
477
|
-
` reverse_proxy web:3000 {`,
|
|
478
|
-
` health_uri /api/health`,
|
|
479
|
-
` health_interval 15s`,
|
|
480
|
-
` }`,
|
|
481
|
-
`}`,
|
|
482
|
-
``,
|
|
483
|
-
`${appHost}:${enginePort} {`,
|
|
484
|
-
` import security_headers`,
|
|
485
|
-
` reverse_proxy engine:3001 {`,
|
|
486
|
-
` health_uri /health`,
|
|
487
|
-
` health_interval 15s`,
|
|
488
|
-
` }`,
|
|
489
|
-
`}`,
|
|
490
|
-
...langfuseHostname ? [
|
|
491
|
-
``,
|
|
492
|
-
`${langfuseHostname} {`,
|
|
493
|
-
` import security_headers`,
|
|
494
|
-
` reverse_proxy langfuse:3000`,
|
|
495
|
-
`}`
|
|
496
|
-
] : [],
|
|
497
|
-
``,
|
|
498
|
-
`# Marketing site \u2014 served independently`,
|
|
499
|
-
`openedgeai.tech {`,
|
|
500
|
-
` import security_headers`,
|
|
501
|
-
` root * /srv/marketing-site`,
|
|
502
|
-
` file_server`,
|
|
503
|
-
` try_files {path} /index.html`,
|
|
504
|
-
`}`
|
|
505
|
-
].join("\n");
|
|
482
|
+
let mcpHostname = "";
|
|
483
|
+
if (config2.mcpEnabled && config2.mcpUrl) {
|
|
484
|
+
try {
|
|
485
|
+
mcpHostname = new URL(config2.mcpUrl).hostname;
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
506
488
|
}
|
|
507
|
-
|
|
489
|
+
const header = [
|
|
508
490
|
`# Lead Routing \u2014 Caddyfile`,
|
|
509
491
|
`# Generated by lead-routing CLI`,
|
|
510
492
|
`# Caddy auto-provisions SSL certificates via Let's Encrypt`,
|
|
@@ -517,7 +499,9 @@ function renderCaddyfile(appUrl, engineUrl, langfuseUrl) {
|
|
|
517
499
|
` Permissions-Policy interest-cohort=()`,
|
|
518
500
|
` Strict-Transport-Security "max-age=31536000; includeSubDomains"`,
|
|
519
501
|
` }`,
|
|
520
|
-
`}
|
|
502
|
+
`}`
|
|
503
|
+
];
|
|
504
|
+
const appBlock = [
|
|
521
505
|
``,
|
|
522
506
|
`${appHost} {`,
|
|
523
507
|
` import security_headers`,
|
|
@@ -525,22 +509,34 @@ function renderCaddyfile(appUrl, engineUrl, langfuseUrl) {
|
|
|
525
509
|
` health_uri /api/health`,
|
|
526
510
|
` health_interval 15s`,
|
|
527
511
|
` }`,
|
|
528
|
-
`}
|
|
512
|
+
`}`
|
|
513
|
+
];
|
|
514
|
+
const engineAddress = engineHost === appHost && enginePort ? `${engineHost}:${enginePort}` : engineHost;
|
|
515
|
+
const engineBlock = [
|
|
529
516
|
``,
|
|
530
|
-
`${
|
|
517
|
+
`${engineAddress} {`,
|
|
531
518
|
` import security_headers`,
|
|
532
519
|
` reverse_proxy engine:3001 {`,
|
|
533
520
|
` health_uri /health`,
|
|
534
521
|
` health_interval 15s`,
|
|
535
522
|
` }`,
|
|
536
|
-
`}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
523
|
+
`}`
|
|
524
|
+
];
|
|
525
|
+
const langfuseBlock = langfuseHostname ? [
|
|
526
|
+
``,
|
|
527
|
+
`${langfuseHostname} {`,
|
|
528
|
+
` import security_headers`,
|
|
529
|
+
` reverse_proxy langfuse:3000`,
|
|
530
|
+
`}`
|
|
531
|
+
] : [];
|
|
532
|
+
const mcpBlock = mcpHostname ? [
|
|
533
|
+
``,
|
|
534
|
+
`${mcpHostname} {`,
|
|
535
|
+
` import security_headers`,
|
|
536
|
+
` reverse_proxy mcp:3100`,
|
|
537
|
+
`}`
|
|
538
|
+
] : [];
|
|
539
|
+
const marketingBlock = [
|
|
544
540
|
``,
|
|
545
541
|
`# Marketing site \u2014 served independently`,
|
|
546
542
|
`openedgeai.tech {`,
|
|
@@ -549,6 +545,14 @@ function renderCaddyfile(appUrl, engineUrl, langfuseUrl) {
|
|
|
549
545
|
` file_server`,
|
|
550
546
|
` try_files {path} /index.html`,
|
|
551
547
|
`}`
|
|
548
|
+
];
|
|
549
|
+
return [
|
|
550
|
+
...header,
|
|
551
|
+
...appBlock,
|
|
552
|
+
...engineBlock,
|
|
553
|
+
...langfuseBlock,
|
|
554
|
+
...mcpBlock,
|
|
555
|
+
...marketingBlock
|
|
552
556
|
].join("\n");
|
|
553
557
|
}
|
|
554
558
|
|
|
@@ -602,12 +606,22 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }, agentApi)
|
|
|
602
606
|
managedLangfuse: !!agentApi,
|
|
603
607
|
langfuseUrl: agentApi?.langfuseUrl,
|
|
604
608
|
langfuseSecret: agentApi?.langfuseSecret,
|
|
605
|
-
langfuseSalt: agentApi?.langfuseSalt
|
|
609
|
+
langfuseSalt: agentApi?.langfuseSalt,
|
|
610
|
+
managedMcp: !!agentApi,
|
|
611
|
+
mcpWebhookSecret: cfg.engineWebhookSecret,
|
|
612
|
+
mcpCrmType: cfg.crmType
|
|
606
613
|
});
|
|
607
614
|
const composeFile = join2(dir, "docker-compose.yml");
|
|
608
615
|
writeFileSync2(composeFile, composeContent, "utf8");
|
|
609
616
|
log3.success("Generated docker-compose.yml");
|
|
610
|
-
const caddyfileContent = renderCaddyfile(
|
|
617
|
+
const caddyfileContent = renderCaddyfile({
|
|
618
|
+
appUrl: cfg.appUrl,
|
|
619
|
+
engineUrl: cfg.engineUrl,
|
|
620
|
+
baseDomain: cfg.baseDomain,
|
|
621
|
+
langfuseUrl: agentApi ? cfg.langfuseUrl : void 0,
|
|
622
|
+
mcpEnabled: !!agentApi,
|
|
623
|
+
mcpUrl: cfg.mcpUrl
|
|
624
|
+
});
|
|
611
625
|
writeFileSync2(join2(dir, "Caddyfile"), caddyfileContent, "utf8");
|
|
612
626
|
log3.success("Generated Caddyfile");
|
|
613
627
|
const envWebContent = renderEnvWeb({
|
|
@@ -650,6 +664,8 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }, agentApi)
|
|
|
650
664
|
writeConfig(dir, {
|
|
651
665
|
appUrl: cfg.appUrl,
|
|
652
666
|
engineUrl: cfg.engineUrl,
|
|
667
|
+
baseDomain: cfg.baseDomain,
|
|
668
|
+
mcpUrl: cfg.mcpUrl,
|
|
653
669
|
crmType: cfg.crmType,
|
|
654
670
|
installDir: dir,
|
|
655
671
|
remoteDir: sshCfg.remoteDir,
|
|
@@ -1502,23 +1518,10 @@ Install URL: ${chalk2.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
|
|
|
1502
1518
|
cancel3("Setup cancelled.");
|
|
1503
1519
|
process.exit(0);
|
|
1504
1520
|
}
|
|
1505
|
-
let langfuseUrl =
|
|
1521
|
+
let langfuseUrl = cfg.langfuseUrl;
|
|
1506
1522
|
let langfuseSecret = "";
|
|
1507
1523
|
let langfuseSalt = "";
|
|
1508
1524
|
if (enableAgentApi) {
|
|
1509
|
-
const appDomain = new URL(cfg.appUrl).hostname;
|
|
1510
|
-
const baseDomain = appDomain.replace(/^app\d*\./, "");
|
|
1511
|
-
const suggestedLangfuse = `evals.${baseDomain}`;
|
|
1512
|
-
const lfUrl = await text3({
|
|
1513
|
-
message: "Langfuse dashboard URL:",
|
|
1514
|
-
initialValue: `https://${suggestedLangfuse}`,
|
|
1515
|
-
placeholder: `https://${suggestedLangfuse}`
|
|
1516
|
-
});
|
|
1517
|
-
if (isCancel3(lfUrl)) {
|
|
1518
|
-
cancel3("Setup cancelled.");
|
|
1519
|
-
process.exit(0);
|
|
1520
|
-
}
|
|
1521
|
-
langfuseUrl = lfUrl.trim();
|
|
1522
1525
|
langfuseSecret = generateSecret(32);
|
|
1523
1526
|
langfuseSalt = generateSecret(16);
|
|
1524
1527
|
}
|
|
@@ -1691,6 +1694,7 @@ Click "Connect HubSpot" to authorize the integration.`,
|
|
|
1691
1694
|
const mcpDir = join5(homedir3(), ".lead-routing");
|
|
1692
1695
|
mkdirSync3(mcpDir, { recursive: true });
|
|
1693
1696
|
const mcpConfig = { appUrl: cfg.appUrl, engineUrl: cfg.engineUrl, webhookSecret, crmType: cfg.crmType || "salesforce" };
|
|
1697
|
+
if (cfg.mcpUrl) mcpConfig.mcpUrl = cfg.mcpUrl;
|
|
1694
1698
|
if (apiToken) mcpConfig.apiToken = apiToken;
|
|
1695
1699
|
writeFileSync4(
|
|
1696
1700
|
join5(mcpDir, "mcp.json"),
|
|
@@ -1709,18 +1713,21 @@ Click "Connect HubSpot" to authorize the integration.`,
|
|
|
1709
1713
|
` : ` ${chalk2.cyan("2.")} Go to Integrations \u2192 HubSpot \u2192 Connect
|
|
1710
1714
|
${chalk2.cyan("3.")} Authorize the HubSpot integration
|
|
1711
1715
|
`;
|
|
1712
|
-
const agentApiLines = enableAgentApi ? `
|
|
1713
|
-
MCP
|
|
1716
|
+
const agentApiLines = enableAgentApi ? ` Evals: ${chalk2.cyan(cfg.langfuseUrl)}
|
|
1717
|
+
MCP: ${chalk2.cyan(cfg.mcpUrl)}
|
|
1718
|
+
MCP config: ~/.lead-routing/mcp.json
|
|
1719
|
+
` : "";
|
|
1720
|
+
const dnsLine = cfg.baseDomain ? `
|
|
1721
|
+
DNS: Add ${chalk2.white(`*.${cfg.baseDomain}`)} -> A -> <server IP>
|
|
1714
1722
|
` : "";
|
|
1715
1723
|
outro(
|
|
1716
1724
|
chalk2.green("\u2714 You're live!") + `
|
|
1717
1725
|
|
|
1718
1726
|
Dashboard: ${chalk2.cyan(cfg.appUrl)}
|
|
1719
|
-
|
|
1727
|
+
Engine: ${chalk2.cyan(cfg.engineUrl)}
|
|
1720
1728
|
` + agentApiLines + `
|
|
1721
1729
|
Admin email: ${chalk2.white(cfg.adminEmail)}
|
|
1722
|
-
|
|
1723
|
-
` + chalk2.bold(" Next steps:\n") + ` ${chalk2.cyan("1.")} Open ${chalk2.cyan(cfg.appUrl)} and log in
|
|
1730
|
+
` + dnsLine + "\n" + chalk2.bold(" Next steps:\n") + ` ${chalk2.cyan("1.")} Open ${chalk2.cyan(cfg.appUrl)} and log in
|
|
1724
1731
|
` + crmSteps + ` ${chalk2.cyan("4.")} Create your first routing rule
|
|
1725
1732
|
|
|
1726
1733
|
Run ${chalk2.cyan("lead-routing doctor")} to check service health at any time.
|