@elmohq/cli 0.2.16 → 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 +109 -30
  2. package/package.json +7 -7
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,14 +391,15 @@ 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",
384
398
  "google-ai-mode",
399
+ "google-ai-overview",
385
400
  "perplexity",
386
401
  "copilot",
387
- "gemini",
388
- "grok"
402
+ "gemini"
389
403
  ];
390
404
  const OLOSTEP_MODELS = [
391
405
  "chatgpt",
@@ -393,21 +407,45 @@ const OLOSTEP_MODELS = [
393
407
  "google-ai-overview",
394
408
  "perplexity",
395
409
  "copilot",
396
- "gemini",
397
- "grok"
410
+ "gemini"
398
411
  ];
399
412
  const OXYLABS_MODELS = [
400
413
  "chatgpt",
401
414
  "google-ai-mode",
415
+ "google-ai-overview",
402
416
  "perplexity"
403
417
  ];
404
- const DEFAULT_SCRAPER_MODELS = ["chatgpt", "google-ai-mode"];
405
- const DATAFORSEO_MODELS = [
406
- "google-ai-mode",
418
+ const CLORO_MODELS = [
407
419
  "chatgpt",
420
+ "google-ai-mode",
421
+ "google-ai-overview",
408
422
  "perplexity",
423
+ "copilot",
409
424
  "gemini"
410
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
+ ];
411
449
  const DEFAULT_OPENAI_MODEL = "gpt-5-mini";
412
450
  const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6";
413
451
  const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
@@ -418,7 +456,8 @@ async function configureProvidersInteractive(env) {
418
456
  "",
419
457
  pc.bold("1. A scraper") + " — to track ChatGPT and Google AI Mode (no public APIs):",
420
458
  ` • ${pc.cyan("BrightData")} — cheap solid option, ~$0.45/mo per prompt`,
421
- ` • ${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`,
422
461
  ` • ${pc.cyan("Olostep")} — premium option, powers Peec/AirOps, ~$2.25/mo per prompt`,
423
462
  "",
424
463
  pc.bold("2. A direct LLM API") + " — for low-latency tasks (onboarding analysis, sentiment scoring,",
@@ -455,7 +494,11 @@ async function configureProvidersRecommended(env) {
455
494
  },
456
495
  {
457
496
  value: "oxylabs",
458
- 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)"
459
502
  },
460
503
  {
461
504
  value: "olostep",
@@ -510,6 +553,7 @@ async function configureProvidersCustom(env) {
510
553
  p.log.step(pc.bold("Step 2 of 2 — Scrapers (optional, but needed to track ChatGPT / Google AI Mode)"));
511
554
  await collectBrightData(env, targets);
512
555
  await collectOxylabs(env, targets);
556
+ await collectCloro(env, targets);
513
557
  await collectOlostep(env, targets);
514
558
  await collectDataForSEO(env, targets);
515
559
  await finalizeScrapeTargets(env, targets);
@@ -540,6 +584,14 @@ async function collectScraperKey(scraper, env) {
540
584
  });
541
585
  assertNotCancelled(password);
542
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;
543
595
  } else {
544
596
  p.log.info(`Sign up: ${link(pc.cyan(OLOSTEP_AFFILIATE), OLOSTEP_AFFILIATE)}`);
545
597
  const key = await p.password({
@@ -625,7 +677,7 @@ async function collectOlostep(env, targets) {
625
677
  }
626
678
  async function collectOxylabs(env, targets) {
627
679
  const enable = await p.confirm({
628
- 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)`,
629
681
  initialValue: false
630
682
  });
631
683
  assertNotCancelled(enable);
@@ -650,6 +702,27 @@ async function collectOxylabs(env, targets) {
650
702
  targets
651
703
  });
652
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
+ }
653
726
  async function pickScraperTargets(args) {
654
727
  const selected = await p.multiselect({
655
728
  message: `LLM Providers to track via ${args.providerLabel}`,
@@ -797,7 +870,7 @@ async function collectOpenRouter(env, targets) {
797
870
  }
798
871
  async function collectDataForSEO(env, targets) {
799
872
  const enable = await p.confirm({
800
- message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode + LLM Responses)`,
873
+ message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode + ChatGPT/Gemini scrapers)`,
801
874
  initialValue: false
802
875
  });
803
876
  assertNotCancelled(enable);
@@ -816,19 +889,23 @@ async function collectDataForSEO(env, targets) {
816
889
  env.DATAFORSEO_PASSWORD = pwd;
817
890
  const selected = await p.multiselect({
818
891
  message: "LLM Providers to track via DataForSEO",
819
- options: DATAFORSEO_MODELS.map((model) => ({
820
- value: model,
821
- 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})`
822
899
  })),
823
900
  required: false,
824
- initialValues: ["google-ai-mode"]
901
+ initialValues: [formatScrapeTarget({
902
+ model: "google-ai-mode",
903
+ provider: "dataforseo",
904
+ webSearch: true
905
+ })]
825
906
  });
826
907
  assertNotCancelled(selected);
827
- for (const model of selected) targets.push(formatScrapeTarget({
828
- model,
829
- provider: "dataforseo",
830
- webSearch: true
831
- }));
908
+ targets.push(...selected);
832
909
  }
833
910
  async function finalizeScrapeTargets(env, targets, options = {}) {
834
911
  const deduped = dedupeTargets(targets);
@@ -1249,12 +1326,13 @@ function sleep(ms) {
1249
1326
  }
1250
1327
  function runDockerCompose(configDir, args) {
1251
1328
  return new Promise((resolve, reject) => {
1252
- spawn("docker", [
1329
+ const commandArgs = [
1253
1330
  "compose",
1254
1331
  "-f",
1255
1332
  path.join(configDir, "elmo.yaml"),
1256
1333
  ...args
1257
- ], { stdio: "inherit" }).on("close", (code) => {
1334
+ ];
1335
+ spawn("docker", commandArgs, { stdio: "inherit" }).on("close", (code) => {
1258
1336
  if (code === 0) resolve();
1259
1337
  else reject(/* @__PURE__ */ new Error(`docker compose exited with code ${code}`));
1260
1338
  });
@@ -1262,12 +1340,13 @@ function runDockerCompose(configDir, args) {
1262
1340
  }
1263
1341
  function runDockerComposeCapture(configDir, args) {
1264
1342
  return new Promise((resolve, reject) => {
1265
- const child = spawn("docker", [
1343
+ const commandArgs = [
1266
1344
  "compose",
1267
1345
  "-f",
1268
1346
  path.join(configDir, "elmo.yaml"),
1269
1347
  ...args
1270
- ]);
1348
+ ];
1349
+ const child = spawn("docker", commandArgs);
1271
1350
  let stdout = "";
1272
1351
  let stderr = "";
1273
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.16",
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.39.2",
35
+ "posthog-node": "^5.47.3",
36
36
  "semver": "^7.8.5"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^26.1.0",
39
+ "@types/node": "^26.1.2",
40
40
  "@types/semver": "^7.7.1",
41
- "rolldown": "^1.1.4",
42
- "typescript": "^6.0.3",
43
- "vitest": "^4.1.9",
44
- "@workspace/config": "0.2.16"
41
+ "rolldown": "^1.2.1",
42
+ "typescript": "^7.0.2",
43
+ "vitest": "^4.1.10",
44
+ "@workspace/config": "0.2.18"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "node scripts/build.mjs",