@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.
- package/dist/index.js +104 -26
- 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
|
-
|
|
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(
|
|
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
|
|
405
|
-
|
|
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")} —
|
|
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 —
|
|
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")}? (
|
|
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 +
|
|
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:
|
|
821
|
-
value:
|
|
822
|
-
|
|
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: [
|
|
901
|
+
initialValues: [formatScrapeTarget({
|
|
902
|
+
model: "google-ai-mode",
|
|
903
|
+
provider: "dataforseo",
|
|
904
|
+
webSearch: true
|
|
905
|
+
})]
|
|
826
906
|
});
|
|
827
907
|
assertNotCancelled(selected);
|
|
828
|
-
|
|
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
|
-
|
|
1329
|
+
const commandArgs = [
|
|
1254
1330
|
"compose",
|
|
1255
1331
|
"-f",
|
|
1256
1332
|
path.join(configDir, "elmo.yaml"),
|
|
1257
1333
|
...args
|
|
1258
|
-
]
|
|
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
|
|
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.
|
|
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.
|
|
35
|
+
"posthog-node": "^5.47.3",
|
|
36
36
|
"semver": "^7.8.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^26.1.
|
|
39
|
+
"@types/node": "^26.1.2",
|
|
40
40
|
"@types/semver": "^7.7.1",
|
|
41
|
-
"rolldown": "^1.1
|
|
41
|
+
"rolldown": "^1.2.1",
|
|
42
42
|
"typescript": "^7.0.2",
|
|
43
43
|
"vitest": "^4.1.10",
|
|
44
|
-
"@workspace/config": "0.2.
|
|
44
|
+
"@workspace/config": "0.2.18"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "node scripts/build.mjs",
|