@elmohq/cli 0.2.3 → 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.
Files changed (3) hide show
  1. package/README.md +89 -0
  2. package/dist/index.js +21 -65
  3. package/package.json +18 -1
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>&nbsp;
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>&nbsp;
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>&nbsp;
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 crypto from "node:crypto";
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",
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
  },