@lead-routing/cli 0.8.1 → 0.8.2

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 +119 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12,6 +12,12 @@ import { join as join5 } from "path";
12
12
  import { intro, outro, note as note3, log as log7, confirm, cancel as cancel3, isCancel as isCancel3, password as promptPassword, select, text as text3 } from "@clack/prompts";
13
13
  import chalk2 from "chalk";
14
14
 
15
+ // src/utils/crypto.ts
16
+ import { randomBytes } from "crypto";
17
+ function generateSecret(bytes = 32) {
18
+ return randomBytes(bytes).toString("hex");
19
+ }
20
+
15
21
  // src/steps/prerequisites.ts
16
22
  import { log } from "@clack/prompts";
17
23
  async function checkPrerequisites() {
@@ -96,14 +102,6 @@ async function collectSshConfig(opts = {}) {
96
102
 
97
103
  // src/steps/collect-config.ts
98
104
  import { text as text2, password as password2, note as note2, cancel as cancel2, isCancel as isCancel2 } from "@clack/prompts";
99
-
100
- // src/utils/crypto.ts
101
- import { randomBytes } from "crypto";
102
- function generateSecret(bytes = 32) {
103
- return randomBytes(bytes).toString("hex");
104
- }
105
-
106
- // src/steps/collect-config.ts
107
105
  function bail2(value) {
108
106
  if (isCancel2(value)) {
109
107
  cancel2("Setup cancelled.");
@@ -281,6 +279,24 @@ function renderDockerCompose(c) {
281
279
  timeout: 5s
282
280
  retries: 6${engineDependsOn}
283
281
  `;
282
+ const langfuseService = c.managedLangfuse ? `
283
+ langfuse:
284
+ image: langfuse/langfuse:2
285
+ restart: unless-stopped
286
+ environment:
287
+ DATABASE_URL: postgresql://leadrouting:${dbPassword}@postgres:5432/langfuse
288
+ NEXTAUTH_URL: ${c.langfuseUrl ?? ""}
289
+ NEXTAUTH_SECRET: ${c.langfuseSecret ?? ""}
290
+ SALT: ${c.langfuseSalt ?? ""}
291
+ TELEMETRY_ENABLED: "false"
292
+ HOSTNAME: "0.0.0.0"
293
+ PORT: "3000"
294
+ depends_on:
295
+ postgres:
296
+ condition: service_healthy
297
+ ` : "";
298
+ const caddyDeps = ["web", "engine"];
299
+ if (c.managedLangfuse) caddyDeps.push("langfuse");
284
300
  const caddyService = `
285
301
  caddy:
286
302
  image: caddy:2-alpine
@@ -294,8 +310,7 @@ function renderDockerCompose(c) {
294
310
  - caddy_data:/data
295
311
  - caddy_config:/config
296
312
  depends_on:
297
- - web
298
- - engine
313
+ ${caddyDeps.map((d) => ` - ${d}`).join("\n")}
299
314
  `;
300
315
  const volumes = buildVolumes(c.managedDb, c.managedRedis);
301
316
  return [
@@ -308,6 +323,7 @@ function renderDockerCompose(c) {
308
323
  redisService.trimEnd(),
309
324
  webService.trimEnd(),
310
325
  engineService.trimEnd(),
326
+ langfuseService.trimEnd(),
311
327
  caddyService.trimEnd(),
312
328
  volumes
313
329
  ].filter(Boolean).join("\n");
@@ -378,6 +394,14 @@ function renderEnvWeb(c) {
378
394
  `HUBSPOT_CLIENT_SECRET=${c.hubspotClientSecret ?? "f95949ac-6464-40d3-bdaa-893c48749951"}`,
379
395
  `HUBSPOT_APP_ID=${c.hubspotAppId ?? "35016223"}`,
380
396
  `HUBSPOT_REDIRECT_URI=${c.appUrl}/api/auth/hubspot/callback`
397
+ ] : [],
398
+ ...c.langfuseEnabled ? [
399
+ ``,
400
+ `# Langfuse (agent evaluation dashboard)`,
401
+ `LANGFUSE_ENABLED=true`,
402
+ `LANGFUSE_URL=http://langfuse:3000`,
403
+ `LANGFUSE_PUBLIC_KEY=${c.langfusePublicKey ?? ""}`,
404
+ `LANGFUSE_SECRET_KEY=${c.langfuseSecretKey ?? ""}`
381
405
  ] : []
382
406
  ].join("\n");
383
407
  }
@@ -419,11 +443,18 @@ function renderEnvEngine(c) {
419
443
  }
420
444
 
421
445
  // src/templates/caddy.ts
422
- function renderCaddyfile(appUrl, engineUrl) {
446
+ function renderCaddyfile(appUrl, engineUrl, langfuseUrl) {
423
447
  const appHost = new URL(appUrl).hostname;
424
448
  const engineParsed = new URL(engineUrl);
425
449
  const engineHost = engineParsed.hostname;
426
450
  const enginePort = engineParsed.port;
451
+ let langfuseHostname = "";
452
+ if (langfuseUrl) {
453
+ try {
454
+ langfuseHostname = new URL(langfuseUrl).hostname;
455
+ } catch {
456
+ }
457
+ }
427
458
  const isSameDomain = engineHost === appHost;
428
459
  if (isSameDomain && enginePort) {
429
460
  return [
@@ -456,6 +487,13 @@ function renderCaddyfile(appUrl, engineUrl) {
456
487
  ` health_interval 15s`,
457
488
  ` }`,
458
489
  `}`,
490
+ ...langfuseHostname ? [
491
+ ``,
492
+ `${langfuseHostname} {`,
493
+ ` import security_headers`,
494
+ ` reverse_proxy langfuse:3000`,
495
+ `}`
496
+ ] : [],
459
497
  ``,
460
498
  `# Marketing site \u2014 served independently`,
461
499
  `openedgeai.tech {`,
@@ -496,6 +534,13 @@ function renderCaddyfile(appUrl, engineUrl) {
496
534
  ` health_interval 15s`,
497
535
  ` }`,
498
536
  `}`,
537
+ ...langfuseHostname ? [
538
+ ``,
539
+ `${langfuseHostname} {`,
540
+ ` import security_headers`,
541
+ ` reverse_proxy langfuse:3000`,
542
+ `}`
543
+ ] : [],
499
544
  ``,
500
545
  `# Marketing site \u2014 served independently`,
501
546
  `openedgeai.tech {`,
@@ -545,7 +590,7 @@ function getCliVersion() {
545
590
  return "0.1.0";
546
591
  }
547
592
  }
548
- function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
593
+ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }, agentApi) {
549
594
  const dir = join2(process.cwd(), "lead-routing");
550
595
  mkdirSync(dir, { recursive: true });
551
596
  const dockerEngineUrl = `http://engine:3001`;
@@ -553,12 +598,16 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
553
598
  managedDb: cfg.managedDb,
554
599
  managedRedis: cfg.managedRedis,
555
600
  dbPassword: cfg.dbPassword,
556
- redisPassword: cfg.redisPassword
601
+ redisPassword: cfg.redisPassword,
602
+ managedLangfuse: !!agentApi,
603
+ langfuseUrl: agentApi?.langfuseUrl,
604
+ langfuseSecret: agentApi?.langfuseSecret,
605
+ langfuseSalt: agentApi?.langfuseSalt
557
606
  });
558
607
  const composeFile = join2(dir, "docker-compose.yml");
559
608
  writeFileSync2(composeFile, composeContent, "utf8");
560
609
  log3.success("Generated docker-compose.yml");
561
- const caddyfileContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
610
+ const caddyfileContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl, agentApi?.langfuseUrl);
562
611
  writeFileSync2(join2(dir, "Caddyfile"), caddyfileContent, "utf8");
563
612
  log3.success("Generated Caddyfile");
564
613
  const envWebContent = renderEnvWeb({
@@ -579,7 +628,8 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
579
628
  crmType: cfg.crmType,
580
629
  hubspotClientId: cfg.hubspotClientId,
581
630
  hubspotClientSecret: cfg.hubspotClientSecret,
582
- hubspotAppId: cfg.hubspotAppId
631
+ hubspotAppId: cfg.hubspotAppId,
632
+ langfuseEnabled: !!agentApi
583
633
  });
584
634
  const envWeb = join2(dir, ".env.web");
585
635
  writeFileSync2(envWeb, envWebContent, "utf8");
@@ -617,6 +667,8 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
617
667
  engineWebhookSecret: cfg.engineWebhookSecret,
618
668
  licenseKey: license.licenseKey,
619
669
  licenseTier: license.licenseTier,
670
+ enableAgentApi: !!agentApi,
671
+ langfuseUrl: agentApi?.langfuseUrl,
620
672
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
621
673
  version: getCliVersion()
622
674
  });
@@ -1442,11 +1494,44 @@ Install URL: ${chalk2.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
1442
1494
  crmType
1443
1495
  }, auth.customer.email, authPassword);
1444
1496
  await checkDnsResolvable(cfg.appUrl, cfg.engineUrl);
1497
+ const enableAgentApi = await confirm({
1498
+ message: "Enable AI agent API? (Langfuse eval dashboard + MCP composite tools)",
1499
+ initialValue: true
1500
+ });
1501
+ if (isCancel3(enableAgentApi)) {
1502
+ cancel3("Setup cancelled.");
1503
+ process.exit(0);
1504
+ }
1505
+ let langfuseUrl = "";
1506
+ let langfuseSecret = "";
1507
+ let langfuseSalt = "";
1508
+ 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
+ langfuseSecret = generateSecret(32);
1523
+ langfuseSalt = generateSecret(16);
1524
+ }
1445
1525
  log7.step("Step 6/9 Generating config files");
1446
1526
  const { dir } = generateFiles(cfg, sshCfg, {
1447
1527
  licenseKey: licenseResult.key,
1448
1528
  licenseTier: licenseResult.tier
1449
- });
1529
+ }, enableAgentApi ? {
1530
+ langfuseUrl,
1531
+ langfuseSecret,
1532
+ langfuseSalt,
1533
+ dbPassword: cfg.dbPassword
1534
+ } : void 0);
1450
1535
  note3(
1451
1536
  `Local config directory: ${chalk2.cyan(dir)}
1452
1537
  Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routing.json`,
@@ -1468,6 +1553,12 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1468
1553
  await uploadFiles(ssh, dir, remoteDir);
1469
1554
  log7.step("Step 8/9 Starting services");
1470
1555
  await startServices(ssh, remoteDir);
1556
+ if (enableAgentApi) {
1557
+ log7.step("Creating Langfuse database...");
1558
+ await ssh.execCommand(`docker exec $(docker ps -qf "name=postgres") psql -U leadrouting -d postgres -c "CREATE DATABASE langfuse OWNER leadrouting;" 2>/dev/null || true`);
1559
+ await ssh.execCommand(`cd ${remoteDir} && docker compose restart langfuse`);
1560
+ log7.success("Langfuse database created");
1561
+ }
1471
1562
  log7.step("Step 9/9 Verifying health");
1472
1563
  let healthy = false;
1473
1564
  while (!healthy) {
@@ -1522,7 +1613,12 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1522
1613
  const { dir: newDir } = generateFiles(cfg, sshCfg, {
1523
1614
  licenseKey: licenseResult.key,
1524
1615
  licenseTier: licenseResult.tier
1525
- });
1616
+ }, enableAgentApi ? {
1617
+ langfuseUrl,
1618
+ langfuseSecret,
1619
+ langfuseSalt,
1620
+ dbPassword: cfg.dbPassword
1621
+ } : void 0);
1526
1622
  await uploadFiles(ssh, newDir, remoteDir);
1527
1623
  log7.step("Restarting services with new config...");
1528
1624
  await startServices(ssh, remoteDir);
@@ -1580,7 +1676,7 @@ Click "Connect HubSpot" to authorize the integration.`,
1580
1676
  "Content-Type": "application/json",
1581
1677
  "Cookie": cookieHeader
1582
1678
  },
1583
- body: JSON.stringify({ name: "Claude Code MCP", scopes: ["read", "write", "route"] })
1679
+ body: JSON.stringify({ name: "Claude Code MCP", scopes: ["read", "write", "route", "agent"] })
1584
1680
  });
1585
1681
  if (tokenRes.ok) {
1586
1682
  const tokenData = await tokenRes.json();
@@ -1594,7 +1690,7 @@ Click "Connect HubSpot" to authorize the integration.`,
1594
1690
  if (webhookSecret) {
1595
1691
  const mcpDir = join5(homedir3(), ".lead-routing");
1596
1692
  mkdirSync3(mcpDir, { recursive: true });
1597
- const mcpConfig = { appUrl: cfg.appUrl, engineUrl: cfg.engineUrl, webhookSecret };
1693
+ const mcpConfig = { appUrl: cfg.appUrl, engineUrl: cfg.engineUrl, webhookSecret, crmType: cfg.crmType || "salesforce" };
1598
1694
  if (apiToken) mcpConfig.apiToken = apiToken;
1599
1695
  writeFileSync4(
1600
1696
  join5(mcpDir, "mcp.json"),
@@ -1613,12 +1709,15 @@ Click "Connect HubSpot" to authorize the integration.`,
1613
1709
  ` : ` ${chalk2.cyan("2.")} Go to Integrations \u2192 HubSpot \u2192 Connect
1614
1710
  ${chalk2.cyan("3.")} Authorize the HubSpot integration
1615
1711
  `;
1712
+ const agentApiLines = enableAgentApi ? ` Langfuse dashboard: ${chalk2.cyan(langfuseUrl)}
1713
+ MCP config: ~/.lead-routing/mcp.json
1714
+ ` : "";
1616
1715
  outro(
1617
1716
  chalk2.green("\u2714 You're live!") + `
1618
1717
 
1619
1718
  Dashboard: ${chalk2.cyan(cfg.appUrl)}
1620
1719
  Routing engine: ${chalk2.cyan(cfg.engineUrl)}
1621
-
1720
+ ` + agentApiLines + `
1622
1721
  Admin email: ${chalk2.white(cfg.adminEmail)}
1623
1722
 
1624
1723
  ` + chalk2.bold(" Next steps:\n") + ` ${chalk2.cyan("1.")} Open ${chalk2.cyan(cfg.appUrl)} and log in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lead-routing/cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Self-hosted deployment CLI for Lead Routing",
5
5
  "homepage": "https://github.com/lead-routing/lead-routing",
6
6
  "keywords": [