@elmohq/cli 0.2.17 → 0.2.18

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 +104 -26
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from "node:child_process";
3
- import crypto from "node:crypto";
3
+ import crypto, { randomBytes } from "node:crypto";
4
4
  import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
@@ -92,7 +92,18 @@ async function runMigrations(plan, ctx) {
92
92
  }
93
93
  //#endregion
94
94
  //#region src/migrations/index.ts
95
- const MIGRATIONS = [];
95
+ const MIGRATIONS = [{
96
+ from: "0.2.17",
97
+ to: "0.2.18",
98
+ description: "add ELMO_ENCRYPTION_KEY for encrypted in-app provider credentials",
99
+ async run(ctx) {
100
+ const env = await ctx.readEnv();
101
+ if (env.ELMO_ENCRYPTION_KEY !== void 0) return;
102
+ env.ELMO_ENCRYPTION_KEY = randomBytes(32).toString("base64");
103
+ await ctx.writeEnv(env);
104
+ ctx.log.info("Added ELMO_ENCRYPTION_KEY — lets you store provider credentials encrypted in the app.");
105
+ }
106
+ }];
96
107
  //#endregion
97
108
  //#region src/telemetry.ts
98
109
  const POSTHOG_PUBLIC_KEY = "phc_Jhx9LnI9cTDFHpQmpOzJSDTW127qD9pFU65KRnYym6z";
@@ -102,7 +113,8 @@ function envDisabled() {
102
113
  }
103
114
  async function readEnvKey(configDir, key) {
104
115
  try {
105
- return parse(await fs.readFile(path.join(configDir, ".env"), "utf8"))[key];
116
+ const contents = await fs.readFile(path.join(configDir, ".env"), "utf8");
117
+ return parse(contents)[key];
106
118
  } catch {
107
119
  return;
108
120
  }
@@ -179,8 +191,8 @@ function assertNotCancelled(value) {
179
191
  process.exit(0);
180
192
  }
181
193
  }
182
- function generateSecret(bytes = 32) {
183
- return crypto.randomBytes(bytes).toString("base64url");
194
+ function generateSecret(bytes = 32, encoding = "base64url") {
195
+ return crypto.randomBytes(bytes).toString(encoding);
184
196
  }
185
197
  function link(text, url) {
186
198
  return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
@@ -274,6 +286,7 @@ async function runInit(options, version) {
274
286
  env.VITE_DEPLOYMENT_MODE = "local";
275
287
  env.DEPLOYMENT_ID = preservedDeploymentId ?? crypto.randomUUID();
276
288
  env.BETTER_AUTH_SECRET = generateSecret();
289
+ env.ELMO_ENCRYPTION_KEY = generateSecret(32, "base64");
277
290
  env.APP_NAME = DEFAULT_APP_NAME;
278
291
  env.APP_ICON = DEFAULT_APP_ICON;
279
292
  env.VITE_APP_NAME = DEFAULT_APP_NAME;
@@ -368,7 +381,7 @@ async function runInit(options, version) {
368
381
  postgres_mode: postgresMode,
369
382
  dev_mode: Boolean(options.dev),
370
383
  setup_mode: setupMode,
371
- has_scraper: Boolean(env.BRIGHTDATA_API_TOKEN || env.OLOSTEP_API_KEY || env.OXYLABS_USERNAME),
384
+ has_scraper: Boolean(env.BRIGHTDATA_API_TOKEN || env.OLOSTEP_API_KEY || env.OXYLABS_USERNAME || env.CLORO_API_KEY),
372
385
  has_direct_api: hasDirectApiConfigured(env)
373
386
  });
374
387
  if (email) await submitNewsletterSignup(configDir, email);
@@ -378,6 +391,7 @@ async function runInit(options, version) {
378
391
  const BRIGHTDATA_AFFILIATE = "https://get.brightdata.com/67h1b7h0shcn";
379
392
  const OLOSTEP_AFFILIATE = "https://olostep.com/?ref=elmo";
380
393
  const OXYLABS_AFFILIATE = "https://oxylabs.go2cloud.org/aff_c?offer_id=7&aff_id=2263&url_id=32";
394
+ const CLORO_AFFILIATE = "https://cloro.dev?fpr=elmo";
381
395
  const PROVIDERS_DOC_URL = "https://docs.elmohq.com/docs/user-guide/providers";
382
396
  const BRIGHTDATA_MODELS = [
383
397
  "chatgpt",
@@ -401,14 +415,37 @@ const OXYLABS_MODELS = [
401
415
  "google-ai-overview",
402
416
  "perplexity"
403
417
  ];
404
- const DEFAULT_SCRAPER_MODELS = ["chatgpt", "google-ai-mode"];
405
- const DATAFORSEO_MODELS = [
418
+ const CLORO_MODELS = [
419
+ "chatgpt",
406
420
  "google-ai-mode",
407
421
  "google-ai-overview",
408
- "chatgpt",
409
422
  "perplexity",
423
+ "copilot",
410
424
  "gemini"
411
425
  ];
426
+ const DEFAULT_SCRAPER_MODELS = ["chatgpt", "google-ai-mode"];
427
+ const DATAFORSEO_TARGETS = [
428
+ {
429
+ model: "google-ai-mode",
430
+ kind: "scraper"
431
+ },
432
+ {
433
+ model: "google-ai-overview",
434
+ kind: "scraper"
435
+ },
436
+ {
437
+ model: "chatgpt",
438
+ kind: "scraper"
439
+ },
440
+ {
441
+ model: "gemini",
442
+ kind: "scraper"
443
+ },
444
+ {
445
+ model: "perplexity",
446
+ kind: "api"
447
+ }
448
+ ];
412
449
  const DEFAULT_OPENAI_MODEL = "gpt-5-mini";
413
450
  const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6";
414
451
  const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
@@ -419,7 +456,8 @@ async function configureProvidersInteractive(env) {
419
456
  "",
420
457
  pc.bold("1. A scraper") + " — to track ChatGPT and Google AI Mode (no public APIs):",
421
458
  ` • ${pc.cyan("BrightData")} — cheap solid option, ~$0.45/mo per prompt`,
422
- ` • ${pc.cyan("Oxylabs")} — sync realtime API, pay-as-you-go`,
459
+ ` • ${pc.cyan("Oxylabs")} — async job API, pay-as-you-go`,
460
+ ` • ${pc.cyan("Cloro")} — every surface, credit plans from $30/mo, ~$0.65/mo per prompt`,
423
461
  ` • ${pc.cyan("Olostep")} — premium option, powers Peec/AirOps, ~$2.25/mo per prompt`,
424
462
  "",
425
463
  pc.bold("2. A direct LLM API") + " — for low-latency tasks (onboarding analysis, sentiment scoring,",
@@ -456,7 +494,11 @@ async function configureProvidersRecommended(env) {
456
494
  },
457
495
  {
458
496
  value: "oxylabs",
459
- label: "Oxylabs — sync realtime API, pay-as-you-go"
497
+ label: "Oxylabs — async job API, pay-as-you-go"
498
+ },
499
+ {
500
+ value: "cloro",
501
+ label: "Cloro — ~$0.65/mo per prompt (credit plans from $30/mo)"
460
502
  },
461
503
  {
462
504
  value: "olostep",
@@ -511,6 +553,7 @@ async function configureProvidersCustom(env) {
511
553
  p.log.step(pc.bold("Step 2 of 2 — Scrapers (optional, but needed to track ChatGPT / Google AI Mode)"));
512
554
  await collectBrightData(env, targets);
513
555
  await collectOxylabs(env, targets);
556
+ await collectCloro(env, targets);
514
557
  await collectOlostep(env, targets);
515
558
  await collectDataForSEO(env, targets);
516
559
  await finalizeScrapeTargets(env, targets);
@@ -541,6 +584,14 @@ async function collectScraperKey(scraper, env) {
541
584
  });
542
585
  assertNotCancelled(password);
543
586
  env.OXYLABS_PASSWORD = password;
587
+ } else if (scraper === "cloro") {
588
+ p.log.info(`Sign up: ${link(pc.cyan(CLORO_AFFILIATE), CLORO_AFFILIATE)}`);
589
+ const key = await p.password({
590
+ message: "Cloro API key",
591
+ validate: (v) => !v ? "Required" : void 0
592
+ });
593
+ assertNotCancelled(key);
594
+ env.CLORO_API_KEY = key;
544
595
  } else {
545
596
  p.log.info(`Sign up: ${link(pc.cyan(OLOSTEP_AFFILIATE), OLOSTEP_AFFILIATE)}`);
546
597
  const key = await p.password({
@@ -626,7 +677,7 @@ async function collectOlostep(env, targets) {
626
677
  }
627
678
  async function collectOxylabs(env, targets) {
628
679
  const enable = await p.confirm({
629
- message: `Configure ${pc.bold("Oxylabs")}? (sync realtime API, pay-as-you-go)`,
680
+ message: `Configure ${pc.bold("Oxylabs")}? (async job API, pay-as-you-go)`,
630
681
  initialValue: false
631
682
  });
632
683
  assertNotCancelled(enable);
@@ -651,6 +702,27 @@ async function collectOxylabs(env, targets) {
651
702
  targets
652
703
  });
653
704
  }
705
+ async function collectCloro(env, targets) {
706
+ const enable = await p.confirm({
707
+ message: `Configure ${pc.bold("Cloro")}? (async task API, credit plans from $30/mo)`,
708
+ initialValue: false
709
+ });
710
+ assertNotCancelled(enable);
711
+ if (!enable) return;
712
+ p.log.info(`Sign up and create an API key: ${link(pc.cyan(CLORO_AFFILIATE), CLORO_AFFILIATE)}`);
713
+ const key = await p.password({
714
+ message: "Cloro API key",
715
+ validate: (v) => !v ? "Required" : void 0
716
+ });
717
+ assertNotCancelled(key);
718
+ env.CLORO_API_KEY = key;
719
+ await pickScraperTargets({
720
+ providerLabel: "Cloro",
721
+ providerId: "cloro",
722
+ allModels: CLORO_MODELS,
723
+ targets
724
+ });
725
+ }
654
726
  async function pickScraperTargets(args) {
655
727
  const selected = await p.multiselect({
656
728
  message: `LLM Providers to track via ${args.providerLabel}`,
@@ -798,7 +870,7 @@ async function collectOpenRouter(env, targets) {
798
870
  }
799
871
  async function collectDataForSEO(env, targets) {
800
872
  const enable = await p.confirm({
801
- message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode + LLM Responses)`,
873
+ message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode + ChatGPT/Gemini scrapers)`,
802
874
  initialValue: false
803
875
  });
804
876
  assertNotCancelled(enable);
@@ -817,19 +889,23 @@ async function collectDataForSEO(env, targets) {
817
889
  env.DATAFORSEO_PASSWORD = pwd;
818
890
  const selected = await p.multiselect({
819
891
  message: "LLM Providers to track via DataForSEO",
820
- options: DATAFORSEO_MODELS.map((model) => ({
821
- value: model,
822
- label: model
892
+ options: DATAFORSEO_TARGETS.map((t) => ({
893
+ value: formatScrapeTarget({
894
+ model: t.model,
895
+ provider: "dataforseo",
896
+ webSearch: true
897
+ }),
898
+ label: `${t.model} (${t.kind})`
823
899
  })),
824
900
  required: false,
825
- initialValues: ["google-ai-mode"]
901
+ initialValues: [formatScrapeTarget({
902
+ model: "google-ai-mode",
903
+ provider: "dataforseo",
904
+ webSearch: true
905
+ })]
826
906
  });
827
907
  assertNotCancelled(selected);
828
- for (const model of selected) targets.push(formatScrapeTarget({
829
- model,
830
- provider: "dataforseo",
831
- webSearch: true
832
- }));
908
+ targets.push(...selected);
833
909
  }
834
910
  async function finalizeScrapeTargets(env, targets, options = {}) {
835
911
  const deduped = dedupeTargets(targets);
@@ -1250,12 +1326,13 @@ function sleep(ms) {
1250
1326
  }
1251
1327
  function runDockerCompose(configDir, args) {
1252
1328
  return new Promise((resolve, reject) => {
1253
- spawn("docker", [
1329
+ const commandArgs = [
1254
1330
  "compose",
1255
1331
  "-f",
1256
1332
  path.join(configDir, "elmo.yaml"),
1257
1333
  ...args
1258
- ], { stdio: "inherit" }).on("close", (code) => {
1334
+ ];
1335
+ spawn("docker", commandArgs, { stdio: "inherit" }).on("close", (code) => {
1259
1336
  if (code === 0) resolve();
1260
1337
  else reject(/* @__PURE__ */ new Error(`docker compose exited with code ${code}`));
1261
1338
  });
@@ -1263,12 +1340,13 @@ function runDockerCompose(configDir, args) {
1263
1340
  }
1264
1341
  function runDockerComposeCapture(configDir, args) {
1265
1342
  return new Promise((resolve, reject) => {
1266
- const child = spawn("docker", [
1343
+ const commandArgs = [
1267
1344
  "compose",
1268
1345
  "-f",
1269
1346
  path.join(configDir, "elmo.yaml"),
1270
1347
  ...args
1271
- ]);
1348
+ ];
1349
+ const child = spawn("docker", commandArgs);
1272
1350
  let stdout = "";
1273
1351
  let stderr = "";
1274
1352
  child.stdout.on("data", (data) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elmohq/cli",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "license": "MIT",
5
5
  "keywords": [
6
6
  "marketing",
@@ -32,16 +32,16 @@
32
32
  "commander": "^15.0.0",
33
33
  "dotenv": "17.4.2",
34
34
  "picocolors": "^1.1.1",
35
- "posthog-node": "^5.41.0",
35
+ "posthog-node": "^5.47.3",
36
36
  "semver": "^7.8.5"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^26.1.1",
39
+ "@types/node": "^26.1.2",
40
40
  "@types/semver": "^7.7.1",
41
- "rolldown": "^1.1.5",
41
+ "rolldown": "^1.2.1",
42
42
  "typescript": "^7.0.2",
43
43
  "vitest": "^4.1.10",
44
- "@workspace/config": "0.2.17"
44
+ "@workspace/config": "0.2.18"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "node scripts/build.mjs",