memro-cli 0.1.0 → 0.1.1

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
@@ -1,15 +1,18 @@
1
- # memro
1
+ # memro-cli
2
2
 
3
3
  Connect your AI tools to [Memro](https://memro.co) memory with one command.
4
4
 
5
5
  ```bash
6
- npx memro connect
6
+ npx memro-cli connect
7
7
  ```
8
8
 
9
- 1. Shows a code and opens the Memro dashboard. Sign in and press **Connect**.
10
- 2. Receives a new API key (revocable in the dashboard → API keys) and stores
9
+ 1. Lists the supported tools with the ones on this machine already ticked.
10
+ Use ↑/↓ to move, space to tick, `a` for all, Enter to confirm (Esc cancels
11
+ without signing in).
12
+ 2. Shows a code and opens the Memro dashboard. Sign in and press **Connect**.
13
+ 3. Receives a new API key (revocable in the dashboard → API keys) and stores
11
14
  it in `~/.memro/credentials.json` (readable only by you).
12
- 3. Finds the tools on this machine and adds the Memro MCP server to each:
15
+ 4. Adds the Memro MCP server to each tool you picked:
13
16
 
14
17
  | Tool | Config written |
15
18
  |---|---|
@@ -29,11 +32,13 @@ once as `<file>.memro-backup`. Restart the tool afterwards.
29
32
  ## Commands
30
33
 
31
34
  ```
32
- npx memro connect [--client cursor,codex] [--yes]
33
- npx memro status
34
- npx memro disconnect [--client ...]
35
+ npx memro-cli connect [--client cursor,codex] [--yes]
36
+ npx memro-cli status
37
+ npx memro-cli disconnect [--client ...]
35
38
  ```
36
39
 
40
+ `--client` picks tools without the list; `--yes` takes every detected tool
41
+ (both work in scripts, where there is no list to show).
37
42
  `--staging`, `--local` or `--api <url>` target another Memro server
38
43
  (self-hosted: `--api https://memro.example.com`).
39
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memro-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Connect Claude Code, Codex, Cursor, Antigravity, OpenCode and other AI tools to Memro memory with one command.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://memro.co",
package/src/cli.js CHANGED
@@ -2,9 +2,9 @@ import { execFile } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import readline from "node:readline/promises";
6
5
 
7
6
  import { CLIENTS, findClient } from "./clients.js";
7
+ import { pick } from "./picker.js";
8
8
 
9
9
  const ENVIRONMENTS = {
10
10
  prod: "https://api.memro.co",
@@ -14,20 +14,20 @@ const ENVIRONMENTS = {
14
14
 
15
15
  const CREDENTIALS = () => path.join(os.homedir(), ".memro", "credentials.json");
16
16
 
17
- const HELP = `memro — connect your AI tools to Memro memory
17
+ const HELP = `memro-cli — connect your AI tools to Memro memory
18
18
 
19
19
  Usage
20
20
  npx memro-cli connect Sign in and add Memro to Claude Code, Codex, Cursor,
21
- Antigravity, OpenCode, Gemini CLI, VS Code, Windsurf
22
- and Claude Desktop
21
+ Antigravity, OpenCode, Gemini CLI, VS Code, Windsurf
22
+ and Claude Desktop
23
23
  npx memro-cli status Show the connected account and configured tools
24
24
  npx memro-cli disconnect Remove Memro from your tools and forget the key
25
25
 
26
26
  Options
27
- --client <ids> Only these tools, comma-separated (${CLIENTS.map((c) => c.id).join(", ")})
28
- --yes, -y Don't ask before writing configs
29
- --staging | --local Use staging or a local server instead of Memro Cloud
30
- --api <url> Use a specific Memro API (self-hosted)
27
+ --client <ids> Only these tools, comma-separated, skipping the list (${CLIENTS.map((c) => c.id).join(", ")})
28
+ --yes, -y Don't ask: use every detected tool
29
+ --staging | --local Use staging or a local server instead of Memro Cloud
30
+ --api <url> Use a specific Memro API (self-hosted)
31
31
  `;
32
32
 
33
33
  function parseArgs(argv) {
@@ -148,20 +148,39 @@ function selectClients(ids) {
148
148
  });
149
149
  }
150
150
 
151
- async function confirm(question) {
152
- if (!process.stdin.isTTY) {
153
- console.log(" Not a terminal: pass --yes to write configs without asking.");
154
- return false;
151
+ /**
152
+ * Which tools to configure: --client wins; --yes takes every detected tool;
153
+ * otherwise the user picks in a list with detected tools pre-selected.
154
+ * Returns null when the user cancels or nothing can be asked.
155
+ */
156
+ async function chooseTargets(opts) {
157
+ if (opts.clients) return selectClients(opts.clients);
158
+ const detected = new Set(CLIENTS.filter((c) => c.detect()));
159
+ if (opts.yes) return [...detected];
160
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
161
+ console.log(" Not a terminal: pass --yes (every detected tool) or --client <ids>.");
162
+ return null;
155
163
  }
156
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
157
- const answer = (await rl.question(question)).trim().toLowerCase();
158
- rl.close();
159
- return answer === "" || answer === "y" || answer === "yes";
164
+ console.log("");
165
+ const picked = await pick(
166
+ CLIENTS.map((c) => ({ label: c.name, hint: detected.has(c) ? "" : "not detected", checked: detected.has(c), client: c })),
167
+ { title: "Which tools should use Memro?" },
168
+ );
169
+ return picked && picked.map((p) => p.client);
160
170
  }
161
171
 
162
172
  async function connect(opts) {
163
- const targets = selectClients(opts.clients);
164
173
  console.log(`\n◆ Memro connect (${opts.api})`);
174
+ // Choose first, so cancelling never creates an API key.
175
+ const targets = await chooseTargets(opts);
176
+ if (!targets) {
177
+ console.log(" Nothing changed.");
178
+ return;
179
+ }
180
+ if (targets.length === 0 && !opts.yes) {
181
+ console.log(" No tools selected. Nothing changed.");
182
+ return;
183
+ }
165
184
 
166
185
  let creds = loadCredentials();
167
186
  if (creds?.api_url === opts.api) {
@@ -185,11 +204,7 @@ async function connect(opts) {
185
204
  printManual(creds);
186
205
  return;
187
206
  }
188
- console.log(`\n Found: ${targets.map((c) => c.name).join(", ")}`);
189
- if (!opts.yes && !(await confirm(" Add Memro to these tools? [Y/n] "))) {
190
- console.log(" Nothing changed.");
191
- return;
192
- }
207
+ console.log(`\n Adding Memro to: ${targets.map((c) => c.name).join(", ")}`);
193
208
 
194
209
  const conn = { mcpUrl: creds.mcp_url, apiKey: creds.api_key };
195
210
  let ok = 0;
package/src/picker.js ADDED
@@ -0,0 +1,91 @@
1
+ // A small multi-select for the terminal: arrow keys (or j/k) move, space
2
+ // toggles, "a" toggles all, Enter confirms, Esc/q/Ctrl-C cancels. No
3
+ // dependencies; the state logic is pure so it can be tested without a TTY.
4
+
5
+ import readline from "node:readline";
6
+
7
+ const ESC = "\x1b[";
8
+ const c = {
9
+ dim: (s) => `${ESC}2m${s}${ESC}22m`,
10
+ bold: (s) => `${ESC}1m${s}${ESC}22m`,
11
+ accent: (s) => `${ESC}38;5;141m${s}${ESC}39m`,
12
+ green: (s) => `${ESC}32m${s}${ESC}39m`,
13
+ };
14
+
15
+ /** Initial state: items are { label, hint?, checked }. */
16
+ export function initialState(items) {
17
+ return { items: items.map((i) => ({ ...i })), cursor: 0, done: false, cancelled: false };
18
+ }
19
+
20
+ /** Apply one key ({ name, ctrl, sequence }) to the state. */
21
+ export function reduce(state, key) {
22
+ const s = { ...state, items: state.items.map((i) => ({ ...i })) };
23
+ const n = s.items.length;
24
+ const name = key.name || key.sequence;
25
+ if ((key.ctrl && name === "c") || name === "escape" || name === "q") {
26
+ s.cancelled = true;
27
+ } else if (name === "return" || name === "enter") {
28
+ s.done = true;
29
+ } else if (name === "up" || name === "k") {
30
+ s.cursor = (s.cursor - 1 + n) % n;
31
+ } else if (name === "down" || name === "j" || name === "tab") {
32
+ s.cursor = (s.cursor + 1) % n;
33
+ } else if (name === "space" || name === " ") {
34
+ s.items[s.cursor].checked = !s.items[s.cursor].checked;
35
+ } else if (name === "a") {
36
+ const all = s.items.every((i) => i.checked);
37
+ s.items.forEach((i) => { i.checked = !all; });
38
+ }
39
+ return s;
40
+ }
41
+
42
+ /** The lines to draw for a state. */
43
+ export function render(state, title) {
44
+ const lines = [` ${c.bold(title)}`, c.dim(" ↑/↓ move · space select · a all · enter confirm · esc cancel"), ""];
45
+ state.items.forEach((item, i) => {
46
+ const here = i === state.cursor;
47
+ const box = item.checked ? c.green("◉") : c.dim("○");
48
+ const label = here ? c.accent(item.label) : item.label;
49
+ const hint = item.hint ? " " + c.dim(item.hint) : "";
50
+ lines.push(` ${here ? c.accent("›") : " "} ${box} ${label}${hint}`);
51
+ });
52
+ const count = state.items.filter((i) => i.checked).length;
53
+ lines.push("", c.dim(` ${count} selected`));
54
+ return lines;
55
+ }
56
+
57
+ /**
58
+ * Show the picker and resolve with the selected items, or null if the user
59
+ * cancelled. Requires an interactive terminal on `input`.
60
+ */
61
+ export function pick(items, { title, input = process.stdin, output = process.stdout } = {}) {
62
+ return new Promise((resolve) => {
63
+ let state = initialState(items);
64
+ let drawn = 0;
65
+ const draw = () => {
66
+ const lines = render(state, title);
67
+ if (drawn) output.write(`${ESC}${drawn}A`); // back to the top of the last frame
68
+ output.write(lines.map((l) => `${ESC}2K${l}`).join("\n") + "\n");
69
+ drawn = lines.length;
70
+ };
71
+ const finish = (value) => {
72
+ input.off("keypress", onKey);
73
+ if (input.isTTY) input.setRawMode(false);
74
+ input.pause();
75
+ output.write(`${ESC}?25h`); // show the cursor again
76
+ resolve(value);
77
+ };
78
+ const onKey = (_str, key = {}) => {
79
+ state = reduce(state, key);
80
+ if (state.cancelled) return finish(null);
81
+ if (state.done) return finish(state.items.filter((i) => i.checked));
82
+ draw();
83
+ };
84
+ readline.emitKeypressEvents(input);
85
+ if (input.isTTY) input.setRawMode(true);
86
+ input.resume();
87
+ output.write(`${ESC}?25l`); // hide the cursor while choosing
88
+ input.on("keypress", onKey);
89
+ draw();
90
+ });
91
+ }