@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.
- package/dist/index.js +119 -20
- 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
|
-
-
|
|
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
|