@mentio-dev/cli 0.1.0 → 0.2.0

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ```bash
6
6
  npm install -g @mentio-dev/cli
7
- mentio auth:set --key mk_live_...
7
+ mentio auth:login
8
8
  mentio keywords:create --term "acme" --kind brand
9
9
  mentio mentions:search --relevant true --sentiment negative --limit 20
10
10
  mentio mentions:update mm_7f3a... --status done
@@ -12,4 +12,4 @@ mentio analytics:summary --range 7d --compare true
12
12
  mentio mentions:watch --platform reddit | jq -r '.post.url'
13
13
  ```
14
14
 
15
- JSON out, compact when piped and indented on a terminal; `--table` for lists. Keys come from `--api-key`, `MENTIO_API_KEY`, or `~/.mentio/config.json`. The full command list is at [docs.mentio.dev/cli/commands](https://docs.mentio.dev/cli/commands).
15
+ JSON out, compact when piped and indented on a terminal; `--table` for lists. `mentio auth:login` signs in through the browser; keys also come from `--api-key`, `MENTIO_API_KEY`, or `mentio auth:set`. Every command, by area, is at [docs.mentio.dev/cli](https://docs.mentio.dev/cli).
package/dist/index.js CHANGED
@@ -8,11 +8,12 @@ var __export = (target, all) => {
8
8
  // src/index.ts
9
9
  import { Command, Option } from "commander";
10
10
  import { writeFileSync as writeFileSync2 } from "fs";
11
+ import { hostname } from "os";
11
12
 
12
13
  // package.json
13
14
  var package_default = {
14
15
  name: "@mentio-dev/cli",
15
- version: "0.1.0",
16
+ version: "0.2.0",
16
17
  description: "Command-line client for the Mentio API: one command per endpoint, plus watch and MCP helpers.",
17
18
  license: "MIT",
18
19
  homepage: "https://docs.mentio.dev/cli",
@@ -132,6 +133,7 @@ function coerceScalar(field, raw, type) {
132
133
  case "integer":
133
134
  case "number": {
134
135
  const n = Number(raw);
136
+ if (!Number.isFinite(n) && /^\d{4}-\d{2}-\d{2}/.test(raw) && Number.isFinite(Date.parse(raw))) return raw;
135
137
  if (raw.trim() === "" || !Number.isFinite(n)) throw new UsageError(`--${field.name} expects a number, got "${raw}"`);
136
138
  if (type === "integer" && !Number.isInteger(n)) throw new UsageError(`--${field.name} expects a whole number, got "${raw}"`);
137
139
  return n;
@@ -224,6 +226,94 @@ function buildRequest(op, positional, flags, jsonBody) {
224
226
  return { path, query, body };
225
227
  }
226
228
 
229
+ // src/login.ts
230
+ import { randomBytes } from "crypto";
231
+ import { spawn } from "child_process";
232
+ import { createServer } from "http";
233
+ var DEFAULT_APP_URL = "https://app.mentio.dev";
234
+ var LoginError = class extends Error {
235
+ constructor(message) {
236
+ super(message);
237
+ this.name = "LoginError";
238
+ }
239
+ };
240
+ var newState = () => randomBytes(24).toString("base64url");
241
+ function loginUrl(appUrl, params) {
242
+ const url = new URL("/cli/authorize", `${appUrl.replace(/\/+$/, "")}/`);
243
+ url.searchParams.set("port", String(params.port));
244
+ url.searchParams.set("state", params.state);
245
+ url.searchParams.set("name", params.name);
246
+ url.searchParams.set("scope", params.scope);
247
+ return url.toString();
248
+ }
249
+ var page = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font:16px system-ui;padding:3rem;max-width:36rem;margin:auto"><h1 style="font-size:1.25rem">${title}</h1><p>${body}</p></body>`;
250
+ function startCallbackServer(state, timeoutMs) {
251
+ return new Promise((resolveServer, rejectServer) => {
252
+ let settle = null;
253
+ const key = new Promise((resolve, reject) => {
254
+ settle = { resolve, reject };
255
+ });
256
+ const server = createServer((req, res) => {
257
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
258
+ if (url.pathname !== "/callback") {
259
+ res.writeHead(404, { "content-type": "text/html; charset=utf-8" }).end(page("Not found", "Nothing here."));
260
+ return;
261
+ }
262
+ const error = url.searchParams.get("error");
263
+ if (error) {
264
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(page("Cancelled", "The CLI was not authorized. You can close this tab."));
265
+ finish(() => settle?.reject(new LoginError(error === "denied" ? "Authorization was declined in the browser." : `Authorization failed: ${error}`)));
266
+ return;
267
+ }
268
+ if (url.searchParams.get("state") !== state) {
269
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(page("State mismatch", "This callback does not belong to the running login. Run mentio auth:login again."));
270
+ return;
271
+ }
272
+ const issued = url.searchParams.get("key");
273
+ if (!issued) {
274
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(page("Missing key", "The callback carried no key. Run mentio auth:login again."));
275
+ return;
276
+ }
277
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(page("Mentio CLI connected", "The key is stored on this computer. You can close this tab and go back to the terminal."));
278
+ finish(() => settle?.resolve(issued));
279
+ });
280
+ const timer = setTimeout(() => finish(() => settle?.reject(new LoginError(`No authorization arrived within ${Math.round(timeoutMs / 1e3)} seconds.`))), timeoutMs);
281
+ const close = () => {
282
+ clearTimeout(timer);
283
+ server.close();
284
+ };
285
+ const finish = (outcome) => {
286
+ setTimeout(() => {
287
+ outcome();
288
+ close();
289
+ }, 50);
290
+ };
291
+ server.on("error", (err) => rejectServer(err));
292
+ server.listen(0, "127.0.0.1", () => {
293
+ const address = server.address();
294
+ if (!address || typeof address === "string") {
295
+ rejectServer(new LoginError("Could not open a loopback port."));
296
+ return;
297
+ }
298
+ key.catch(() => {
299
+ });
300
+ resolveServer({ port: address.port, key, close });
301
+ });
302
+ });
303
+ }
304
+ function openBrowser(url) {
305
+ try {
306
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
307
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
308
+ child.on("error", () => {
309
+ });
310
+ child.unref();
311
+ return true;
312
+ } catch {
313
+ return false;
314
+ }
315
+ }
316
+
227
317
  // src/mcp.ts
228
318
  var DEFAULT_MCP_URL = "https://mcp.mentio.dev/mcp";
229
319
  function mcpConfig(kind, url, apiKey) {
@@ -3662,8 +3752,8 @@ async function watchMentions(options) {
3662
3752
  if (result.error !== void 0 || !result.response?.ok) {
3663
3753
  options.warn(JSON.stringify({ error: result.error ?? { code: `http_${result.response?.status ?? 0}`, message: "poll failed" } }));
3664
3754
  } else {
3665
- const page = result.data?.data ?? [];
3666
- const fresh = takeNew(seen, page);
3755
+ const page2 = result.data?.data ?? [];
3756
+ const fresh = takeNew(seen, page2);
3667
3757
  if (!first || options.fromStart) for (const item of fresh) options.write(JSON.stringify(item));
3668
3758
  }
3669
3759
  first = false;
@@ -3743,6 +3833,30 @@ async function readStdin() {
3743
3833
  return Buffer.concat(chunks).toString("utf8");
3744
3834
  }
3745
3835
  function registerAuth() {
3836
+ program.command("auth:login").description("Sign in through the browser: the dashboard mints an API key and hands it to this terminal").option("--app-url <url>", "Dashboard URL", DEFAULT_APP_URL).option("--name <label>", "Name of the key the dashboard creates", `CLI on ${hostname()}`).addOption(new Option("--scope <scope>", "read: GET only. write: everything.").choices(["read", "write"]).default("write")).option("--timeout <seconds>", "How long to wait for the browser", "300").option("--no-open", "Print the URL instead of opening the browser").action(async (flags) => {
3837
+ const globals = program.opts();
3838
+ const seconds = Number(flags.timeout);
3839
+ if (!Number.isFinite(seconds) || seconds < 10) fail({ error: { code: "usage", message: "--timeout must be at least 10 seconds" } }, 2);
3840
+ const state = newState();
3841
+ const server = await startCallbackServer(state, seconds * 1e3);
3842
+ const url = loginUrl(flags.appUrl, { port: server.port, state, name: flags.name, scope: flags.scope });
3843
+ const opened = flags.open ? openBrowser(url) : false;
3844
+ process.stderr.write(`${opened ? "Opening your browser to authorize the CLI. If it does not open, visit:" : "Open this URL to authorize the CLI:"}
3845
+ ${url}
3846
+ Waiting for the browser (${seconds}s)...
3847
+ `);
3848
+ let key;
3849
+ try {
3850
+ key = await server.key;
3851
+ } catch (err) {
3852
+ fail({ error: { code: "login_failed", message: err instanceof LoginError ? err.message : String(err) } });
3853
+ }
3854
+ const path = writeConfig({ apiKey: key, ...globals.apiUrl ? { apiUrl: globals.apiUrl } : {} });
3855
+ const settings = resolveSettings({ apiKey: key, apiUrl: globals.apiUrl });
3856
+ const result = await clientFor(settings).request({ method: "GET", url: "/v1/company", security: BEARER, throwOnError: false });
3857
+ const company = result.error === void 0 && result.response?.ok ? result.data : void 0;
3858
+ print({ ok: true, workspace: company?.name ?? null, key: keyPrefix(key), scope: flags.scope, file: path }, globals);
3859
+ });
3746
3860
  program.command("auth:set").description("Store an API key (and optionally the API host) in ~/.mentio/config.json").requiredOption("--key <key>", "API key from the dashboard or POST /v1/api-keys (mk_live_...)").option("--url <url>", "API host for a self-hosted deployment").action((flags) => {
3747
3861
  const path = writeConfig({ apiKey: flags.key, ...flags.url ? { apiUrl: flags.url } : {} });
3748
3862
  print({ ok: true, file: path, key: keyPrefix(flags.key) }, program.opts());