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 +13 -8
- package/package.json +1 -1
- package/src/cli.js +37 -22
- package/src/picker.js +91 -0
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.
|
|
10
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
22
|
-
|
|
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>
|
|
28
|
-
--yes, -y
|
|
29
|
-
--staging | --local
|
|
30
|
-
--api <url>
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
+
}
|