@elmohq/cli 0.2.2 → 0.2.4
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/README.md +89 -0
- package/dist/index.js +21 -65
- package/package.json +19 -2
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/elmohq/elmo">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/elmohq/elmo/main/apps/www/public/brand/logos/elmo-logo-xl.png" alt="Elmo" width="300">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
The official CLI for <a href="https://www.elmohq.com/">Elmo</a> — open source AI visibility tracking and optimization.
|
|
9
|
+
<br />
|
|
10
|
+
<br />
|
|
11
|
+
<a href="https://www.elmohq.com/docs"><strong>Read the docs »</strong></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<br />
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://www.npmjs.com/package/@elmohq/cli"><img src="https://img.shields.io/npm/v/@elmohq/cli?color=2563eb&label=npm" alt="npm version"></a>
|
|
18
|
+
<a href="https://www.elmohq.com/docs"><img src="https://img.shields.io/badge/Docs-2563eb?style=flat&logo=readthedocs&logoColor=white" alt="Docs"></a>
|
|
19
|
+
<a href="https://github.com/elmohq/elmo"><img src="https://img.shields.io/github/stars/elmohq/elmo?style=flat&logo=github&color=ee964b&label=Star" alt="GitHub stars"></a>
|
|
20
|
+
<a href="https://discord.gg/s24nubCtKz"><img src="https://img.shields.io/badge/Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord"></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<br />
|
|
24
|
+
|
|
25
|
+
## What is Elmo?
|
|
26
|
+
|
|
27
|
+
[Elmo](https://www.elmohq.com/) is an open source platform for tracking and optimizing how your brand shows up in AI assistants like ChatGPT, Claude, Gemini, and Perplexity. Define the prompts your customers ask, run them across providers on a schedule, and get visibility into mentions, sentiment, citations, and competitor positioning over time.
|
|
28
|
+
|
|
29
|
+
`@elmohq/cli` is the fastest way to run Elmo on your own infrastructure. It generates a Docker Compose stack, manages secrets and configuration, and gives you a single command to start, stop, and operate your instance.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g @elmohq/cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires [Docker](https://docs.docker.com/get-docker/) and Docker Compose.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# 1. Walk through the interactive setup wizard
|
|
43
|
+
elmo init
|
|
44
|
+
|
|
45
|
+
# 2. Start the stack
|
|
46
|
+
elmo start
|
|
47
|
+
|
|
48
|
+
# 3. Open the app at http://localhost:1515
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`elmo init` will prompt you for a few things (database, AI provider credentials), generate `elmo.yaml` and `.env`, and optionally start the stack for you.
|
|
52
|
+
|
|
53
|
+
For the full self-hosting walkthrough, see the [Elmo docs](https://www.elmohq.com/docs).
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `elmo init` | Interactive wizard to set up a local Elmo instance |
|
|
60
|
+
| `elmo start` | Start the Elmo stack |
|
|
61
|
+
| `elmo stop` | Stop the Elmo stack |
|
|
62
|
+
| `elmo status` | Check the health of running services |
|
|
63
|
+
| `elmo logs [service]` | Tail container logs (pass `-f` to follow) |
|
|
64
|
+
| `elmo regen` | Regenerate `elmo.yaml` / `.env` from your saved config |
|
|
65
|
+
| `elmo compose <args...>` | Run any `docker compose` command against your Elmo project |
|
|
66
|
+
| `elmo build` | Build Docker images locally (for `--dev` installs) |
|
|
67
|
+
|
|
68
|
+
Run `elmo --help` or `elmo <command> --help` for the full list of flags.
|
|
69
|
+
|
|
70
|
+
### Useful flags
|
|
71
|
+
|
|
72
|
+
- `--dir <path>` — point any command at a specific config directory (defaults to `~/.config/elmo`).
|
|
73
|
+
- `elmo init --dev` — build images from a local checkout of the repo instead of pulling from the registry.
|
|
74
|
+
|
|
75
|
+
## Telemetry
|
|
76
|
+
|
|
77
|
+
The CLI sends anonymous install and command events so we can understand which flows people use and where setup breaks. To opt out, set:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export DISABLE_TELEMETRY=1
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Star, contribute, and chat
|
|
84
|
+
|
|
85
|
+
Elmo is built in the open and we'd love your help.
|
|
86
|
+
|
|
87
|
+
- ⭐ **[Star us on GitHub](https://github.com/elmohq/elmo)** — it genuinely helps more people find the project.
|
|
88
|
+
- 💬 **[Join the Discord](https://discord.gg/s24nubCtKz)** — get help, share what you're building, and talk to the team.
|
|
89
|
+
- 🐛 **[File an issue](https://github.com/elmohq/elmo/issues)** if something breaks or you have a feature request.
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import pc from "picocolors";
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import crypto from "node:crypto";
|
|
6
4
|
import fs from "node:fs/promises";
|
|
7
|
-
import path from "node:path";
|
|
8
5
|
import os from "node:os";
|
|
9
|
-
import
|
|
10
|
-
import semver from "semver";
|
|
6
|
+
import path from "node:path";
|
|
11
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import semver from "semver";
|
|
12
12
|
import { trackCliEvent } from "./telemetry.js";
|
|
13
13
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
14
14
|
const CONFIG_HOME = path.join(os.homedir(), ".config", "elmo");
|
|
15
15
|
const CONFIG_FILE = path.join(CONFIG_HOME, "config.json");
|
|
16
|
-
const DEFAULT_ORG_ID = "default";
|
|
17
|
-
const DEFAULT_ORG_NAME = "Default Organization";
|
|
18
16
|
const DEFAULT_APP_NAME = "Elmo";
|
|
19
17
|
const DEFAULT_APP_ICON = "/icons/elmo-icon.svg";
|
|
20
18
|
const DEFAULT_APP_URL = "http://localhost:1515";
|
|
@@ -220,8 +218,6 @@ async function runInit(options, version) {
|
|
|
220
218
|
env.DEPLOYMENT_MODE = "local";
|
|
221
219
|
env.VITE_DEPLOYMENT_MODE = "local";
|
|
222
220
|
env.BETTER_AUTH_SECRET = generateSecret();
|
|
223
|
-
env.DEFAULT_ORG_ID = DEFAULT_ORG_ID;
|
|
224
|
-
env.DEFAULT_ORG_NAME = DEFAULT_ORG_NAME;
|
|
225
221
|
env.APP_NAME = DEFAULT_APP_NAME;
|
|
226
222
|
env.APP_ICON = DEFAULT_APP_ICON;
|
|
227
223
|
env.APP_URL = DEFAULT_APP_URL;
|
|
@@ -302,14 +298,7 @@ const BRIGHTDATA_AFFILIATE = "https://get.brightdata.com/67h1b7h0shcn";
|
|
|
302
298
|
const OLOSTEP_AFFILIATE = "https://olostep.com/?ref=elmo";
|
|
303
299
|
const PROVIDERS_DOC_URL = "https://docs.elmohq.com/docs/deployment/providers";
|
|
304
300
|
// Surfaces each scraper can track — the first two are the "recommended starter" set.
|
|
305
|
-
const BRIGHTDATA_MODELS = [
|
|
306
|
-
"chatgpt",
|
|
307
|
-
"google-ai-mode",
|
|
308
|
-
"perplexity",
|
|
309
|
-
"copilot",
|
|
310
|
-
"gemini",
|
|
311
|
-
"grok",
|
|
312
|
-
];
|
|
301
|
+
const BRIGHTDATA_MODELS = ["chatgpt", "google-ai-mode", "perplexity", "copilot", "gemini", "grok"];
|
|
313
302
|
const OLOSTEP_MODELS = [
|
|
314
303
|
"chatgpt",
|
|
315
304
|
"google-ai-mode",
|
|
@@ -813,8 +802,7 @@ function buildWebService(options) {
|
|
|
813
802
|
if (options.dependsOn.length > 0) {
|
|
814
803
|
lines.push(" depends_on:");
|
|
815
804
|
for (const service of options.dependsOn) {
|
|
816
|
-
const condition = options.dependencyConditions[service] ??
|
|
817
|
-
"service_started";
|
|
805
|
+
const condition = options.dependencyConditions[service] ?? "service_started";
|
|
818
806
|
lines.push(` ${service}:`, ` condition: ${condition}`);
|
|
819
807
|
}
|
|
820
808
|
}
|
|
@@ -832,8 +820,7 @@ function buildWorkerService(options) {
|
|
|
832
820
|
if (options.dependsOn.length > 0) {
|
|
833
821
|
lines.push(" depends_on:");
|
|
834
822
|
for (const service of options.dependsOn) {
|
|
835
|
-
const condition = options.dependencyConditions[service] ??
|
|
836
|
-
"service_started";
|
|
823
|
+
const condition = options.dependencyConditions[service] ?? "service_started";
|
|
837
824
|
lines.push(` ${service}:`, ` condition: ${condition}`);
|
|
838
825
|
}
|
|
839
826
|
}
|
|
@@ -848,11 +835,7 @@ function indentBlock(block, spaces) {
|
|
|
848
835
|
}
|
|
849
836
|
// ── Docker Helpers ───────────────────────────────────────────────────────────
|
|
850
837
|
async function getComposeServices(configDir) {
|
|
851
|
-
const output = await runDockerComposeCapture(configDir, [
|
|
852
|
-
"ps",
|
|
853
|
-
"--format",
|
|
854
|
-
"json",
|
|
855
|
-
]);
|
|
838
|
+
const output = await runDockerComposeCapture(configDir, ["ps", "--format", "json"]);
|
|
856
839
|
if (!output.trim()) {
|
|
857
840
|
return [];
|
|
858
841
|
}
|
|
@@ -889,8 +872,7 @@ function formatServiceStatus(service) {
|
|
|
889
872
|
if (isServiceReady(service)) {
|
|
890
873
|
color = pc.green;
|
|
891
874
|
}
|
|
892
|
-
else if (health === "starting" ||
|
|
893
|
-
state.includes("starting")) {
|
|
875
|
+
else if (health === "starting" || state.includes("starting")) {
|
|
894
876
|
color = pc.yellow;
|
|
895
877
|
}
|
|
896
878
|
return color(`${label} ${state} ${service.Health ? `(${health})` : ""}`.trim());
|
|
@@ -908,8 +890,7 @@ async function waitForHealthy(configDir, timeoutMs) {
|
|
|
908
890
|
const start = Date.now();
|
|
909
891
|
while (Date.now() - start < timeoutMs) {
|
|
910
892
|
const services = await getComposeServices(configDir);
|
|
911
|
-
if (services.length > 0 &&
|
|
912
|
-
services.every(isServiceReady)) {
|
|
893
|
+
if (services.length > 0 && services.every(isServiceReady)) {
|
|
913
894
|
return true;
|
|
914
895
|
}
|
|
915
896
|
await sleep(3000);
|
|
@@ -922,12 +903,7 @@ function sleep(ms) {
|
|
|
922
903
|
function runDockerCompose(configDir, args) {
|
|
923
904
|
return new Promise((resolve, reject) => {
|
|
924
905
|
const composeFile = path.join(configDir, "elmo.yaml");
|
|
925
|
-
const commandArgs = [
|
|
926
|
-
"compose",
|
|
927
|
-
"-f",
|
|
928
|
-
composeFile,
|
|
929
|
-
...args,
|
|
930
|
-
];
|
|
906
|
+
const commandArgs = ["compose", "-f", composeFile, ...args];
|
|
931
907
|
const child = spawn("docker", commandArgs, {
|
|
932
908
|
stdio: "inherit",
|
|
933
909
|
});
|
|
@@ -944,12 +920,7 @@ function runDockerCompose(configDir, args) {
|
|
|
944
920
|
function runDockerComposeCapture(configDir, args) {
|
|
945
921
|
return new Promise((resolve, reject) => {
|
|
946
922
|
const composeFile = path.join(configDir, "elmo.yaml");
|
|
947
|
-
const commandArgs = [
|
|
948
|
-
"compose",
|
|
949
|
-
"-f",
|
|
950
|
-
composeFile,
|
|
951
|
-
...args,
|
|
952
|
-
];
|
|
923
|
+
const commandArgs = ["compose", "-f", composeFile, ...args];
|
|
953
924
|
const child = spawn("docker", commandArgs);
|
|
954
925
|
let stdout = "";
|
|
955
926
|
let stderr = "";
|
|
@@ -964,8 +935,7 @@ function runDockerComposeCapture(configDir, args) {
|
|
|
964
935
|
resolve(stdout);
|
|
965
936
|
}
|
|
966
937
|
else {
|
|
967
|
-
reject(new Error(stderr ||
|
|
968
|
-
`docker compose exited with code ${code}`));
|
|
938
|
+
reject(new Error(stderr || `docker compose exited with code ${code}`));
|
|
969
939
|
}
|
|
970
940
|
});
|
|
971
941
|
});
|
|
@@ -982,11 +952,7 @@ function assertDockerRunning() {
|
|
|
982
952
|
async function resolveDockerDirInteractive(cwd) {
|
|
983
953
|
const inCwd = await fileExists(path.join(cwd, "Dockerfile"));
|
|
984
954
|
const inDockerDir = await fileExists(path.join(cwd, "docker", "Dockerfile"));
|
|
985
|
-
const defaultDir = inCwd
|
|
986
|
-
? "."
|
|
987
|
-
: inDockerDir
|
|
988
|
-
? "docker"
|
|
989
|
-
: ".";
|
|
955
|
+
const defaultDir = inCwd ? "." : inDockerDir ? "docker" : ".";
|
|
990
956
|
const dir = await p.text({
|
|
991
957
|
message: "Path to docker directory (contains Dockerfile)",
|
|
992
958
|
defaultValue: defaultDir,
|
|
@@ -1063,11 +1029,7 @@ async function writeConfigFiles(configDir, initConfig) {
|
|
|
1063
1029
|
await fs.writeFile(composePath, initConfig.composeYaml, "utf8");
|
|
1064
1030
|
}
|
|
1065
1031
|
function buildEnvFile(env) {
|
|
1066
|
-
const lines = [
|
|
1067
|
-
"# Generated by elmo init",
|
|
1068
|
-
"# WARNING: contains secrets. Do not commit.",
|
|
1069
|
-
"",
|
|
1070
|
-
];
|
|
1032
|
+
const lines = ["# Generated by elmo init", "# WARNING: contains secrets. Do not commit.", ""];
|
|
1071
1033
|
for (const [key, rawValue] of Object.entries(env)) {
|
|
1072
1034
|
if (rawValue === undefined) {
|
|
1073
1035
|
continue;
|
|
@@ -1081,9 +1043,7 @@ function formatEnvValue(value) {
|
|
|
1081
1043
|
return '""';
|
|
1082
1044
|
}
|
|
1083
1045
|
if (/[\s#"']/u.test(value)) {
|
|
1084
|
-
const escaped = value
|
|
1085
|
-
.replace(/\\/g, "\\\\")
|
|
1086
|
-
.replace(/"/g, '\\"');
|
|
1046
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1087
1047
|
return `"${escaped}"`;
|
|
1088
1048
|
}
|
|
1089
1049
|
return value;
|
|
@@ -1091,9 +1051,7 @@ function formatEnvValue(value) {
|
|
|
1091
1051
|
async function detectSettingsFromConfig(configDir) {
|
|
1092
1052
|
const envPath = path.join(configDir, ".env");
|
|
1093
1053
|
const env = await readEnvFile(envPath);
|
|
1094
|
-
const postgresMode = env.DATABASE_URL?.includes("postgres:5432")
|
|
1095
|
-
? "docker"
|
|
1096
|
-
: "external";
|
|
1054
|
+
const postgresMode = env.DATABASE_URL?.includes("postgres:5432") ? "docker" : "external";
|
|
1097
1055
|
const yamlPath = path.join(configDir, "elmo.yaml");
|
|
1098
1056
|
let dev = false;
|
|
1099
1057
|
let repoRoot = process.cwd();
|
|
@@ -1124,8 +1082,7 @@ async function readEnvFile(envPath) {
|
|
|
1124
1082
|
const key = trimmed.substring(0, eqIndex);
|
|
1125
1083
|
let value = trimmed.substring(eqIndex + 1);
|
|
1126
1084
|
// Remove surrounding quotes
|
|
1127
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
1128
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
1085
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
1129
1086
|
value = value.slice(1, -1);
|
|
1130
1087
|
}
|
|
1131
1088
|
env[key] = value;
|
|
@@ -1166,8 +1123,7 @@ async function maybeNotifyNewVersion(currentVersion) {
|
|
|
1166
1123
|
if (!data.version) {
|
|
1167
1124
|
return;
|
|
1168
1125
|
}
|
|
1169
|
-
if (semver.valid(currentVersion) &&
|
|
1170
|
-
semver.lt(currentVersion, data.version)) {
|
|
1126
|
+
if (semver.valid(currentVersion) && semver.lt(currentVersion, data.version)) {
|
|
1171
1127
|
log.warn(`New CLI version available (${data.version}). Run: npm install -g @elmohq/cli@latest`);
|
|
1172
1128
|
}
|
|
1173
1129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elmohq/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"marketing",
|
|
6
|
+
"typescript",
|
|
7
|
+
"tools",
|
|
8
|
+
"aio",
|
|
9
|
+
"seo",
|
|
10
|
+
"geo",
|
|
11
|
+
"gemini",
|
|
12
|
+
"copilot",
|
|
13
|
+
"grok",
|
|
14
|
+
"aso",
|
|
15
|
+
"perplexity",
|
|
16
|
+
"aeo",
|
|
17
|
+
"llmo",
|
|
18
|
+
"chatgpt",
|
|
19
|
+
"ai-visibility"
|
|
20
|
+
],
|
|
4
21
|
"bin": {
|
|
5
22
|
"elmo": "dist/index.js"
|
|
6
23
|
},
|
|
@@ -19,7 +36,7 @@
|
|
|
19
36
|
"devDependencies": {
|
|
20
37
|
"@types/node": "^25.6.0",
|
|
21
38
|
"@types/semver": "^7.7.1",
|
|
22
|
-
"typescript": "^
|
|
39
|
+
"typescript": "^6.0.3"
|
|
23
40
|
},
|
|
24
41
|
"scripts": {
|
|
25
42
|
"build": "tsc -p tsconfig.json"
|