@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.
- package/dist/index.js +109 -30
- 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
|
-
|
|
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,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
|
|
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")} —
|
|
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 —
|
|
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")}? (
|
|
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 +
|
|
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:
|
|
820
|
-
value:
|
|
821
|
-
|
|
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: [
|
|
901
|
+
initialValues: [formatScrapeTarget({
|
|
902
|
+
model: "google-ai-mode",
|
|
903
|
+
provider: "dataforseo",
|
|
904
|
+
webSearch: true
|
|
905
|
+
})]
|
|
825
906
|
});
|
|
826
907
|
assertNotCancelled(selected);
|
|
827
|
-
|
|
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
|
-
|
|
1329
|
+
const commandArgs = [
|
|
1253
1330
|
"compose",
|
|
1254
1331
|
"-f",
|
|
1255
1332
|
path.join(configDir, "elmo.yaml"),
|
|
1256
1333
|
...args
|
|
1257
|
-
]
|
|
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
|
|
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.
|
|
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
|
|
42
|
-
"typescript": "^
|
|
43
|
-
"vitest": "^4.1.
|
|
44
|
-
"@workspace/config": "0.2.
|
|
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",
|