@lead-routing/cli 0.8.0 → 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.
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.");
@@ -111,7 +109,7 @@ function bail2(value) {
111
109
  }
112
110
  throw new Error("Unexpected cancel");
113
111
  }
114
- async function collectConfig(opts = {}, authEmail) {
112
+ async function collectConfig(opts = {}, authEmail, authPassword) {
115
113
  const crmType = opts.crmType ?? "salesforce";
116
114
  note2(
117
115
  "You will need:\n \u2022 Public HTTPS URLs for the web app and routing engine",
@@ -168,14 +166,19 @@ async function collectConfig(opts = {}, authEmail) {
168
166
  });
169
167
  if (isCancel2(adminEmail)) bail2(adminEmail);
170
168
  }
171
- const adminPassword = await password2({
172
- message: "Admin password (min 8 characters)",
173
- validate: (v) => {
174
- if (!v) return "Required";
175
- if (v.length < 8) return "Must be at least 8 characters";
176
- }
177
- });
178
- if (isCancel2(adminPassword)) bail2(adminPassword);
169
+ let adminPassword;
170
+ if (authPassword) {
171
+ adminPassword = authPassword;
172
+ } else {
173
+ adminPassword = await password2({
174
+ message: "Admin password (min 8 characters)",
175
+ validate: (v) => {
176
+ if (!v) return "Required";
177
+ if (v.length < 8) return "Must be at least 8 characters";
178
+ }
179
+ });
180
+ if (isCancel2(adminPassword)) bail2(adminPassword);
181
+ }
179
182
  const sessionSecret = generateSecret(32);
180
183
  const engineWebhookSecret = generateSecret(32);
181
184
  const internalApiKey = generateSecret(32);
@@ -276,6 +279,24 @@ function renderDockerCompose(c) {
276
279
  timeout: 5s
277
280
  retries: 6${engineDependsOn}
278
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");
279
300
  const caddyService = `
280
301
  caddy:
281
302
  image: caddy:2-alpine
@@ -289,8 +310,7 @@ function renderDockerCompose(c) {
289
310
  - caddy_data:/data
290
311
  - caddy_config:/config
291
312
  depends_on:
292
- - web
293
- - engine
313
+ ${caddyDeps.map((d) => ` - ${d}`).join("\n")}
294
314
  `;
295
315
  const volumes = buildVolumes(c.managedDb, c.managedRedis);
296
316
  return [
@@ -303,6 +323,7 @@ function renderDockerCompose(c) {
303
323
  redisService.trimEnd(),
304
324
  webService.trimEnd(),
305
325
  engineService.trimEnd(),
326
+ langfuseService.trimEnd(),
306
327
  caddyService.trimEnd(),
307
328
  volumes
308
329
  ].filter(Boolean).join("\n");
@@ -373,6 +394,14 @@ function renderEnvWeb(c) {
373
394
  `HUBSPOT_CLIENT_SECRET=${c.hubspotClientSecret ?? "f95949ac-6464-40d3-bdaa-893c48749951"}`,
374
395
  `HUBSPOT_APP_ID=${c.hubspotAppId ?? "35016223"}`,
375
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 ?? ""}`
376
405
  ] : []
377
406
  ].join("\n");
378
407
  }
@@ -414,11 +443,18 @@ function renderEnvEngine(c) {
414
443
  }
415
444
 
416
445
  // src/templates/caddy.ts
417
- function renderCaddyfile(appUrl, engineUrl) {
446
+ function renderCaddyfile(appUrl, engineUrl, langfuseUrl) {
418
447
  const appHost = new URL(appUrl).hostname;
419
448
  const engineParsed = new URL(engineUrl);
420
449
  const engineHost = engineParsed.hostname;
421
450
  const enginePort = engineParsed.port;
451
+ let langfuseHostname = "";
452
+ if (langfuseUrl) {
453
+ try {
454
+ langfuseHostname = new URL(langfuseUrl).hostname;
455
+ } catch {
456
+ }
457
+ }
422
458
  const isSameDomain = engineHost === appHost;
423
459
  if (isSameDomain && enginePort) {
424
460
  return [
@@ -451,6 +487,13 @@ function renderCaddyfile(appUrl, engineUrl) {
451
487
  ` health_interval 15s`,
452
488
  ` }`,
453
489
  `}`,
490
+ ...langfuseHostname ? [
491
+ ``,
492
+ `${langfuseHostname} {`,
493
+ ` import security_headers`,
494
+ ` reverse_proxy langfuse:3000`,
495
+ `}`
496
+ ] : [],
454
497
  ``,
455
498
  `# Marketing site \u2014 served independently`,
456
499
  `openedgeai.tech {`,
@@ -491,6 +534,13 @@ function renderCaddyfile(appUrl, engineUrl) {
491
534
  ` health_interval 15s`,
492
535
  ` }`,
493
536
  `}`,
537
+ ...langfuseHostname ? [
538
+ ``,
539
+ `${langfuseHostname} {`,
540
+ ` import security_headers`,
541
+ ` reverse_proxy langfuse:3000`,
542
+ `}`
543
+ ] : [],
494
544
  ``,
495
545
  `# Marketing site \u2014 served independently`,
496
546
  `openedgeai.tech {`,
@@ -540,7 +590,7 @@ function getCliVersion() {
540
590
  return "0.1.0";
541
591
  }
542
592
  }
543
- function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
593
+ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }, agentApi) {
544
594
  const dir = join2(process.cwd(), "lead-routing");
545
595
  mkdirSync(dir, { recursive: true });
546
596
  const dockerEngineUrl = `http://engine:3001`;
@@ -548,12 +598,16 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
548
598
  managedDb: cfg.managedDb,
549
599
  managedRedis: cfg.managedRedis,
550
600
  dbPassword: cfg.dbPassword,
551
- redisPassword: cfg.redisPassword
601
+ redisPassword: cfg.redisPassword,
602
+ managedLangfuse: !!agentApi,
603
+ langfuseUrl: agentApi?.langfuseUrl,
604
+ langfuseSecret: agentApi?.langfuseSecret,
605
+ langfuseSalt: agentApi?.langfuseSalt
552
606
  });
553
607
  const composeFile = join2(dir, "docker-compose.yml");
554
608
  writeFileSync2(composeFile, composeContent, "utf8");
555
609
  log3.success("Generated docker-compose.yml");
556
- const caddyfileContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl);
610
+ const caddyfileContent = renderCaddyfile(cfg.appUrl, cfg.engineUrl, agentApi?.langfuseUrl);
557
611
  writeFileSync2(join2(dir, "Caddyfile"), caddyfileContent, "utf8");
558
612
  log3.success("Generated Caddyfile");
559
613
  const envWebContent = renderEnvWeb({
@@ -574,7 +628,8 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
574
628
  crmType: cfg.crmType,
575
629
  hubspotClientId: cfg.hubspotClientId,
576
630
  hubspotClientSecret: cfg.hubspotClientSecret,
577
- hubspotAppId: cfg.hubspotAppId
631
+ hubspotAppId: cfg.hubspotAppId,
632
+ langfuseEnabled: !!agentApi
578
633
  });
579
634
  const envWeb = join2(dir, ".env.web");
580
635
  writeFileSync2(envWeb, envWebContent, "utf8");
@@ -612,6 +667,8 @@ function generateFiles(cfg, sshCfg, license = { licenseTier: "free" }) {
612
667
  engineWebhookSecret: cfg.engineWebhookSecret,
613
668
  licenseKey: license.licenseKey,
614
669
  licenseTier: license.licenseTier,
670
+ enableAgentApi: !!agentApi,
671
+ langfuseUrl: agentApi?.langfuseUrl,
615
672
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
616
673
  version: getCliVersion()
617
674
  });
@@ -1245,6 +1302,7 @@ async function runInit(options = {}) {
1245
1302
  return;
1246
1303
  }
1247
1304
  let auth;
1305
+ let authPassword;
1248
1306
  try {
1249
1307
  auth = await requireAuth();
1250
1308
  } catch {
@@ -1314,6 +1372,7 @@ After verifying, press Enter to continue.`,
1314
1372
  }
1315
1373
  saveCredentials({ token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() });
1316
1374
  auth = { token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() };
1375
+ authPassword = signupPw;
1317
1376
  } catch (err) {
1318
1377
  log7.error(err instanceof Error ? err.message : "Login failed");
1319
1378
  process.exit(1);
@@ -1344,6 +1403,7 @@ After verifying, press Enter to continue.`,
1344
1403
  }
1345
1404
  saveCredentials({ token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() });
1346
1405
  auth = { token, customer, storedAt: (/* @__PURE__ */ new Date()).toISOString() };
1406
+ authPassword = loginPw;
1347
1407
  } catch (err) {
1348
1408
  log7.error(err instanceof Error ? err.message : "Login failed");
1349
1409
  process.exit(1);
@@ -1432,13 +1492,46 @@ Install URL: ${chalk2.cyan(MANAGED_PACKAGE_INSTALL_URL)}`,
1432
1492
  externalDb: options.externalDb,
1433
1493
  externalRedis: options.externalRedis,
1434
1494
  crmType
1435
- }, auth.customer.email);
1495
+ }, auth.customer.email, authPassword);
1436
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
+ }
1437
1525
  log7.step("Step 6/9 Generating config files");
1438
1526
  const { dir } = generateFiles(cfg, sshCfg, {
1439
1527
  licenseKey: licenseResult.key,
1440
1528
  licenseTier: licenseResult.tier
1441
- });
1529
+ }, enableAgentApi ? {
1530
+ langfuseUrl,
1531
+ langfuseSecret,
1532
+ langfuseSalt,
1533
+ dbPassword: cfg.dbPassword
1534
+ } : void 0);
1442
1535
  note3(
1443
1536
  `Local config directory: ${chalk2.cyan(dir)}
1444
1537
  Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routing.json`,
@@ -1460,6 +1553,12 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1460
1553
  await uploadFiles(ssh, dir, remoteDir);
1461
1554
  log7.step("Step 8/9 Starting services");
1462
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
+ }
1463
1562
  log7.step("Step 9/9 Verifying health");
1464
1563
  let healthy = false;
1465
1564
  while (!healthy) {
@@ -1514,7 +1613,12 @@ Files created: docker-compose.yml, Caddyfile, .env.web, .env.engine, lead-routin
1514
1613
  const { dir: newDir } = generateFiles(cfg, sshCfg, {
1515
1614
  licenseKey: licenseResult.key,
1516
1615
  licenseTier: licenseResult.tier
1517
- });
1616
+ }, enableAgentApi ? {
1617
+ langfuseUrl,
1618
+ langfuseSecret,
1619
+ langfuseSalt,
1620
+ dbPassword: cfg.dbPassword
1621
+ } : void 0);
1518
1622
  await uploadFiles(ssh, newDir, remoteDir);
1519
1623
  log7.step("Restarting services with new config...");
1520
1624
  await startServices(ssh, remoteDir);
@@ -1572,7 +1676,7 @@ Click "Connect HubSpot" to authorize the integration.`,
1572
1676
  "Content-Type": "application/json",
1573
1677
  "Cookie": cookieHeader
1574
1678
  },
1575
- body: JSON.stringify({ name: "Claude Code MCP", scopes: ["read", "write", "route"] })
1679
+ body: JSON.stringify({ name: "Claude Code MCP", scopes: ["read", "write", "route", "agent"] })
1576
1680
  });
1577
1681
  if (tokenRes.ok) {
1578
1682
  const tokenData = await tokenRes.json();
@@ -1586,7 +1690,7 @@ Click "Connect HubSpot" to authorize the integration.`,
1586
1690
  if (webhookSecret) {
1587
1691
  const mcpDir = join5(homedir3(), ".lead-routing");
1588
1692
  mkdirSync3(mcpDir, { recursive: true });
1589
- const mcpConfig = { appUrl: cfg.appUrl, engineUrl: cfg.engineUrl, webhookSecret };
1693
+ const mcpConfig = { appUrl: cfg.appUrl, engineUrl: cfg.engineUrl, webhookSecret, crmType: cfg.crmType || "salesforce" };
1590
1694
  if (apiToken) mcpConfig.apiToken = apiToken;
1591
1695
  writeFileSync4(
1592
1696
  join5(mcpDir, "mcp.json"),
@@ -1605,12 +1709,15 @@ Click "Connect HubSpot" to authorize the integration.`,
1605
1709
  ` : ` ${chalk2.cyan("2.")} Go to Integrations \u2192 HubSpot \u2192 Connect
1606
1710
  ${chalk2.cyan("3.")} Authorize the HubSpot integration
1607
1711
  `;
1712
+ const agentApiLines = enableAgentApi ? ` Langfuse dashboard: ${chalk2.cyan(langfuseUrl)}
1713
+ MCP config: ~/.lead-routing/mcp.json
1714
+ ` : "";
1608
1715
  outro(
1609
1716
  chalk2.green("\u2714 You're live!") + `
1610
1717
 
1611
1718
  Dashboard: ${chalk2.cyan(cfg.appUrl)}
1612
1719
  Routing engine: ${chalk2.cyan(cfg.engineUrl)}
1613
-
1720
+ ` + agentApiLines + `
1614
1721
  Admin email: ${chalk2.white(cfg.adminEmail)}
1615
1722
 
1616
1723
  ` + chalk2.bold(" Next steps:\n") + ` ${chalk2.cyan("1.")} Open ${chalk2.cyan(cfg.appUrl)} and log in
@@ -0,0 +1,76 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "FlowStatus" AS ENUM ('DRAFT', 'ACTIVE', 'INACTIVE');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "FlowNodeType" AS ENUM ('ENTRY', 'DECISION', 'BRANCH_DECISION', 'MATCH', 'ASSIGNMENT', 'UPDATE_FIELD', 'CREATE_TASK', 'FILTER', 'DEFAULT');
6
+
7
+ -- AlterTable
8
+ ALTER TABLE "organizations" ADD COLUMN "routingMode" JSONB;
9
+
10
+ -- AlterTable
11
+ ALTER TABLE "routing_logs" ADD COLUMN "flowId" TEXT,
12
+ ADD COLUMN "flowNodePath" JSONB;
13
+
14
+ -- CreateTable
15
+ CREATE TABLE "routing_flows" (
16
+ "id" TEXT NOT NULL,
17
+ "orgId" TEXT NOT NULL,
18
+ "objectType" "SfdcObjectType" NOT NULL,
19
+ "name" TEXT NOT NULL DEFAULT 'Untitled Flow',
20
+ "status" "FlowStatus" NOT NULL DEFAULT 'DRAFT',
21
+ "triggerEvent" "TriggerEvent" NOT NULL DEFAULT 'BOTH',
22
+ "isDryRun" BOOLEAN NOT NULL DEFAULT false,
23
+ "version" INTEGER NOT NULL DEFAULT 1,
24
+ "publishedAt" TIMESTAMP(3),
25
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
26
+ "updatedAt" TIMESTAMP(3) NOT NULL,
27
+
28
+ CONSTRAINT "routing_flows_pkey" PRIMARY KEY ("id")
29
+ );
30
+
31
+ -- CreateTable
32
+ CREATE TABLE "flow_nodes" (
33
+ "id" TEXT NOT NULL,
34
+ "flowId" TEXT NOT NULL,
35
+ "type" "FlowNodeType" NOT NULL,
36
+ "label" TEXT,
37
+ "positionX" DOUBLE PRECISION NOT NULL DEFAULT 0,
38
+ "positionY" DOUBLE PRECISION NOT NULL DEFAULT 0,
39
+ "config" JSONB,
40
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
41
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
42
+ "updatedAt" TIMESTAMP(3) NOT NULL,
43
+
44
+ CONSTRAINT "flow_nodes_pkey" PRIMARY KEY ("id")
45
+ );
46
+
47
+ -- CreateTable
48
+ CREATE TABLE "flow_edges" (
49
+ "id" TEXT NOT NULL,
50
+ "flowId" TEXT NOT NULL,
51
+ "fromId" TEXT NOT NULL,
52
+ "toId" TEXT NOT NULL,
53
+ "label" TEXT,
54
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
55
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
56
+
57
+ CONSTRAINT "flow_edges_pkey" PRIMARY KEY ("id")
58
+ );
59
+
60
+ -- CreateIndex
61
+ CREATE UNIQUE INDEX "routing_flows_orgId_objectType_key" ON "routing_flows"("orgId", "objectType");
62
+
63
+ -- AddForeignKey
64
+ ALTER TABLE "routing_flows" ADD CONSTRAINT "routing_flows_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
65
+
66
+ -- AddForeignKey
67
+ ALTER TABLE "flow_nodes" ADD CONSTRAINT "flow_nodes_flowId_fkey" FOREIGN KEY ("flowId") REFERENCES "routing_flows"("id") ON DELETE CASCADE ON UPDATE CASCADE;
68
+
69
+ -- AddForeignKey
70
+ ALTER TABLE "flow_edges" ADD CONSTRAINT "flow_edges_flowId_fkey" FOREIGN KEY ("flowId") REFERENCES "routing_flows"("id") ON DELETE CASCADE ON UPDATE CASCADE;
71
+
72
+ -- AddForeignKey
73
+ ALTER TABLE "flow_edges" ADD CONSTRAINT "flow_edges_fromId_fkey" FOREIGN KEY ("fromId") REFERENCES "flow_nodes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
74
+
75
+ -- AddForeignKey
76
+ ALTER TABLE "flow_edges" ADD CONSTRAINT "flow_edges_toId_fkey" FOREIGN KEY ("toId") REFERENCES "flow_nodes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
1
+ -- AlterTable
2
+ ALTER TABLE "flow_edges" ADD COLUMN "sourceHandle" TEXT;
3
+ ALTER TABLE "flow_edges" ADD COLUMN "targetHandle" TEXT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lead-routing/cli",
3
- "version": "0.8.0",
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": [