@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.
Files changed (2) hide show
  1. package/dist/index.js +122 -115
  2. 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 Public HTTPS URLs for the web app and routing engine",
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 appUrl = await text2({
119
- message: "App URL (public URL where the web app will be accessible)",
120
- placeholder: "https://routing.acme.com",
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 "Required";
123
- try {
124
- const u = new URL(v);
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(appUrl)) bail2(appUrl);
132
- const crmLabel = crmType === "hubspot" ? "HubSpot" : "Salesforce";
133
- const engineUrl = await text2({
134
- message: `Engine URL (public URL ${crmLabel} will use to route leads)`,
135
- placeholder: "https://engine.acme.com or https://acme.com:3001",
136
- validate: (v) => {
137
- if (!v) return "Required";
138
- try {
139
- const u = new URL(v);
140
- if (u.protocol !== "https:") return `Must be an HTTPS URL (${crmLabel} requires HTTPS)`;
141
- } catch {
142
- return "Must be a valid URL (e.g. https://engine.acme.com)";
143
- }
144
- }
145
- });
146
- if (isCancel2(engineUrl)) bail2(engineUrl);
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: appUrl.trim().replace(/\/+$/, ""),
190
- engineUrl: engineUrl.trim().replace(/\/+$/, ""),
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(appUrl, engineUrl, langfuseUrl) {
447
- const appHost = new URL(appUrl).hostname;
448
- const engineParsed = new URL(engineUrl);
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
- const isSameDomain = engineHost === appHost;
459
- if (isSameDomain && enginePort) {
460
- return [
461
- `# Lead Routing \u2014 Caddyfile`,
462
- `# Generated by lead-routing CLI`,
463
- `# Caddy auto-provisions SSL certificates via Let's Encrypt`,
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
- return [
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
- `${engineHost} {`,
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
- ...langfuseHostname ? [
538
- ``,
539
- `${langfuseHostname} {`,
540
- ` import security_headers`,
541
- ` reverse_proxy langfuse:3000`,
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(cfg.appUrl, cfg.engineUrl, agentApi?.langfuseUrl);
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 ? ` Langfuse dashboard: ${chalk2.cyan(langfuseUrl)}
1713
- MCP config: ~/.lead-routing/mcp.json
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
- Routing engine: ${chalk2.cyan(cfg.engineUrl)}
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lead-routing/cli",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Self-hosted deployment CLI for Lead Routing",
5
5
  "homepage": "https://github.com/lead-routing/lead-routing",
6
6
  "keywords": [