memro-cli 0.1.0 → 0.1.5
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 +82 -24
- package/package.json +2 -2
- package/src/cli.js +358 -78
- package/src/clients.js +152 -55
- package/src/instructions.js +109 -0
- package/src/picker.js +173 -0
- package/src/ui.js +131 -0
package/src/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, execFileSync } 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, pickOne, promptText } from "./picker.js";
|
|
8
|
+
import { installInstructions, uninstallInstructions } from "./instructions.js";
|
|
9
|
+
import * as ui from "./ui.js";
|
|
8
10
|
|
|
9
11
|
const ENVIRONMENTS = {
|
|
10
12
|
prod: "https://api.memro.co",
|
|
@@ -14,38 +16,75 @@ const ENVIRONMENTS = {
|
|
|
14
16
|
|
|
15
17
|
const CREDENTIALS = () => path.join(os.homedir(), ".memro", "credentials.json");
|
|
16
18
|
|
|
17
|
-
const HELP = `memro — connect your AI tools to Memro memory
|
|
19
|
+
const HELP = `memro-cli — connect your AI tools to Memro memory
|
|
18
20
|
|
|
19
21
|
Usage
|
|
20
22
|
npx memro-cli connect Sign in and add Memro to Claude Code, Codex, Cursor,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
Antigravity, OpenCode, Gemini CLI, VS Code, Windsurf
|
|
24
|
+
and Claude Desktop
|
|
23
25
|
npx memro-cli status Show the connected account and configured tools
|
|
24
26
|
npx memro-cli disconnect Remove Memro from your tools and forget the key
|
|
25
27
|
|
|
26
28
|
Options
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
29
|
+
--project Configure the current project instead of global tools
|
|
30
|
+
--scope <user|project> Configuration scope: "user" (default) or "project"
|
|
31
|
+
--client <ids> Only these tools, comma-separated (${CLIENTS.map((c) => c.id).join(", ")})
|
|
32
|
+
--key <key> Use a specific API key (memro_sk_...) instead of saved credentials
|
|
33
|
+
--yes, -y Don't ask: use every detected tool
|
|
34
|
+
--no-instructions Don't add the "use Memro automatically" note to
|
|
35
|
+
the tools' instructions (CLAUDE.md, AGENTS.md…)
|
|
36
|
+
--staging | --local Use staging or a local server instead of Memro Cloud
|
|
37
|
+
--api <url> Use a specific Memro API (self-hosted)
|
|
31
38
|
`;
|
|
32
39
|
|
|
33
|
-
function parseArgs(argv) {
|
|
34
|
-
const opts = { command: null, clients: null, yes: false, api: null };
|
|
40
|
+
export function parseArgs(argv) {
|
|
41
|
+
const opts = { command: null, clients: null, yes: false, api: null, instructions: true, scope: null, key: null };
|
|
42
|
+
let projectFlag = false;
|
|
43
|
+
let scopeFlagValue = null;
|
|
44
|
+
|
|
35
45
|
for (let i = 0; i < argv.length; i++) {
|
|
36
46
|
const a = argv[i];
|
|
37
|
-
if (a === "--client" || a === "--clients")
|
|
38
|
-
|
|
39
|
-
else if (a === "--
|
|
40
|
-
|
|
41
|
-
else if (a === "--
|
|
47
|
+
if (a === "--client" || a === "--clients") {
|
|
48
|
+
opts.clients = (argv[++i] || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
49
|
+
} else if (a === "--yes" || a === "-y") {
|
|
50
|
+
opts.yes = true;
|
|
51
|
+
} else if (a === "--no-instructions") {
|
|
52
|
+
opts.instructions = false;
|
|
53
|
+
} else if (a === "--project") {
|
|
54
|
+
projectFlag = true;
|
|
55
|
+
} else if (a === "--scope") {
|
|
56
|
+
const val = (argv[++i] || "").toLowerCase().trim();
|
|
57
|
+
if (!val || val.startsWith("-")) throw new Error("--scope requires a value ('user' or 'project')");
|
|
58
|
+
if (val !== "user" && val !== "project") throw new Error(`Invalid scope "${val}". Must be "user" or "project".`);
|
|
59
|
+
if (scopeFlagValue && scopeFlagValue !== val) {
|
|
60
|
+
throw new Error(`Conflicting scope arguments: specified both "--scope ${scopeFlagValue}" and "--scope ${val}".`);
|
|
61
|
+
}
|
|
62
|
+
scopeFlagValue = val;
|
|
63
|
+
} else if (a === "--key") {
|
|
64
|
+
opts.key = argv[++i];
|
|
65
|
+
if (!opts.key || opts.key.startsWith("-")) throw new Error("--key requires an API key (memro_sk_...)");
|
|
66
|
+
} else if (a === "--staging") {
|
|
67
|
+
opts.api = ENVIRONMENTS.staging;
|
|
68
|
+
} else if (a === "--local") {
|
|
69
|
+
opts.api = ENVIRONMENTS.local;
|
|
70
|
+
} else if (a === "--api") {
|
|
42
71
|
opts.api = argv[++i];
|
|
43
72
|
if (!opts.api || opts.api.startsWith("-")) throw new Error("--api needs a URL, e.g. --api https://memro.example.com");
|
|
73
|
+
} else if (a === "--help" || a === "-h") {
|
|
74
|
+
opts.command = "help";
|
|
75
|
+
} else if (!opts.command) {
|
|
76
|
+
opts.command = a;
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error(`Unexpected argument: ${a}`);
|
|
44
79
|
}
|
|
45
|
-
else if (a === "--help" || a === "-h") opts.command = "help";
|
|
46
|
-
else if (!opts.command) opts.command = a;
|
|
47
|
-
else throw new Error(`Unexpected argument: ${a}`);
|
|
48
80
|
}
|
|
81
|
+
|
|
82
|
+
if (projectFlag && scopeFlagValue === "user") {
|
|
83
|
+
throw new Error('Conflicting scope arguments: cannot combine "--project" with "--scope user".');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
opts.scopeExplicit = projectFlag || Boolean(scopeFlagValue);
|
|
87
|
+
opts.scope = projectFlag ? "project" : (scopeFlagValue || "user");
|
|
49
88
|
opts.api = checkURL((opts.api || process.env.MEMRO_API_URL || ENVIRONMENTS.prod).replace(/\/+$/, ""), "API");
|
|
50
89
|
return opts;
|
|
51
90
|
}
|
|
@@ -95,14 +134,15 @@ async function deviceLogin(api) {
|
|
|
95
134
|
if (!start.ok) throw new Error(`Couldn't start sign-in: ${start.data?.message || start.status}`);
|
|
96
135
|
const { device_code, user_code, verification_uri_complete, expires_in, interval } = start.data;
|
|
97
136
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
137
|
+
ui.info("Sign in to Memro");
|
|
138
|
+
ui.line(`Your code: ${ui.c.bold(user_code)}`);
|
|
139
|
+
ui.line(ui.c.dim(`Opening ${verification_uri_complete}`));
|
|
140
|
+
ui.line(ui.c.dim("Approve it there. If the browser didn't open, visit the link."));
|
|
101
141
|
openBrowser(verification_uri_complete);
|
|
102
142
|
|
|
103
143
|
const deadline = Date.now() + (expires_in || 600) * 1000;
|
|
104
144
|
let wait = (interval || 3) * 1000;
|
|
105
|
-
process.stdout.write("
|
|
145
|
+
process.stdout.write(`${ui.bar()} ${ui.c.dim("Waiting for approval…")}`);
|
|
106
146
|
while (Date.now() < deadline) {
|
|
107
147
|
await sleep(wait);
|
|
108
148
|
let res;
|
|
@@ -112,7 +152,7 @@ async function deviceLogin(api) {
|
|
|
112
152
|
continue; // a dropped connection: keep polling until the code expires
|
|
113
153
|
}
|
|
114
154
|
if (res.ok) {
|
|
115
|
-
process.stdout.write("
|
|
155
|
+
process.stdout.write(` ${ui.c.green("approved")}\n`);
|
|
116
156
|
checkURL(res.data.mcp_url, "MCP");
|
|
117
157
|
return res.data;
|
|
118
158
|
}
|
|
@@ -139,110 +179,342 @@ function loadCredentials() {
|
|
|
139
179
|
try { return JSON.parse(fs.readFileSync(CREDENTIALS(), "utf8")); } catch { return null; }
|
|
140
180
|
}
|
|
141
181
|
|
|
142
|
-
function
|
|
143
|
-
|
|
182
|
+
export function isGitTracked(dir = process.cwd()) {
|
|
183
|
+
try {
|
|
184
|
+
return fs.existsSync(path.join(dir, ".git")) ||
|
|
185
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, stdio: "ignore" }) !== null;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The home folder or the filesystem root: never a project. */
|
|
192
|
+
export function isHomeOrRoot(dir) {
|
|
193
|
+
// Compare real paths: git reports /private/var/… where HOME says /var/…,
|
|
194
|
+
// and C:/Users/name where Windows may give the 8.3 C:\Users\NAME~1. The
|
|
195
|
+
// native realpath expands short names; Windows paths ignore case.
|
|
196
|
+
const real = (p) => {
|
|
197
|
+
let r;
|
|
198
|
+
try { r = fs.realpathSync.native(path.resolve(p)); } catch { r = path.resolve(p); }
|
|
199
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
200
|
+
};
|
|
201
|
+
const d = real(dir);
|
|
202
|
+
return d === real(os.homedir()) || d === real(path.parse(d).root);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function resolveProjectRoot(startDir = process.cwd()) {
|
|
206
|
+
try {
|
|
207
|
+
const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
208
|
+
cwd: startDir,
|
|
209
|
+
encoding: "utf8",
|
|
210
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
211
|
+
}).trim();
|
|
212
|
+
if (toplevel && fs.existsSync(toplevel) && !isHomeOrRoot(toplevel)) {
|
|
213
|
+
return toplevel;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Git not available or not inside a work tree
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Walk up checking for project boundary markers
|
|
220
|
+
let curr = path.resolve(startDir);
|
|
221
|
+
const homeDir = path.resolve(os.homedir());
|
|
222
|
+
while (true) {
|
|
223
|
+
// Stop if we reach user's home directory: home folder is never a project root
|
|
224
|
+
if (curr === homeDir) break;
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
fs.existsSync(path.join(curr, ".git")) ||
|
|
228
|
+
fs.existsSync(path.join(curr, "CLAUDE.md")) ||
|
|
229
|
+
fs.existsSync(path.join(curr, "package.json")) ||
|
|
230
|
+
fs.existsSync(path.join(curr, "go.mod")) ||
|
|
231
|
+
fs.existsSync(path.join(curr, "Cargo.toml")) ||
|
|
232
|
+
fs.existsSync(path.join(curr, "pyproject.toml"))
|
|
233
|
+
) {
|
|
234
|
+
return curr;
|
|
235
|
+
}
|
|
236
|
+
const parent = path.dirname(curr);
|
|
237
|
+
if (parent === curr) break;
|
|
238
|
+
curr = parent;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return path.resolve(startDir);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function ensureGitIgnored(filePath, projectRoot) {
|
|
245
|
+
const rel = path.relative(projectRoot, filePath);
|
|
246
|
+
const pattern = "/" + rel.split(path.sep).join("/");
|
|
247
|
+
|
|
248
|
+
// Check if git considers it ignored already
|
|
249
|
+
try {
|
|
250
|
+
execFileSync("git", ["check-ignore", "-q", rel], { cwd: projectRoot, stdio: "ignore" });
|
|
251
|
+
return { ignored: true, added: false };
|
|
252
|
+
} catch {
|
|
253
|
+
// Not ignored or git not found
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check if .gitignore already has this pattern
|
|
257
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
258
|
+
try {
|
|
259
|
+
const text = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
260
|
+
const lines = text.split("\n").map((l) => l.trim());
|
|
261
|
+
if (lines.includes(pattern) || lines.includes(pattern.slice(1)) || lines.includes(pattern + "/") || lines.includes(pattern.slice(1) + "/")) {
|
|
262
|
+
return { ignored: true, added: false };
|
|
263
|
+
}
|
|
264
|
+
const dirPart = rel.split(path.sep)[0];
|
|
265
|
+
if (lines.includes(`/${dirPart}/`) || lines.includes(`/${dirPart}`) || lines.includes(`${dirPart}/`) || lines.includes(dirPart)) {
|
|
266
|
+
return { ignored: true, added: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const addition = (text && !text.endsWith("\n") ? "\n" : "") +
|
|
270
|
+
`# Memro MCP configuration (contains API key)\n${pattern}\n`;
|
|
271
|
+
fs.writeFileSync(gitignorePath, text + addition);
|
|
272
|
+
return { ignored: true, added: true };
|
|
273
|
+
} catch {
|
|
274
|
+
return { ignored: false, added: false };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function selectClients(ids, scope = "user", cwd = process.cwd()) {
|
|
279
|
+
if (!ids) return CLIENTS.filter((c) => c.detect({ scope, cwd }) && c.supportsScope(scope));
|
|
144
280
|
return ids.map((id) => {
|
|
145
281
|
const c = findClient(id);
|
|
146
282
|
if (!c) throw new Error(`Unknown client "${id}". Known: ${CLIENTS.map((x) => x.id).join(", ")}`);
|
|
283
|
+
if (!c.supportsScope(scope)) throw new Error(`${c.name} does not support ${scope} scope.`);
|
|
147
284
|
return c;
|
|
148
285
|
});
|
|
149
286
|
}
|
|
150
287
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Which tools to configure: --client wins; --yes takes every detected tool;
|
|
290
|
+
* otherwise the user picks in a list with detected tools pre-selected.
|
|
291
|
+
* Returns null when the user cancels or nothing can be asked.
|
|
292
|
+
*/
|
|
293
|
+
async function chooseTargets(opts, cwd = process.cwd()) {
|
|
294
|
+
const scope = opts.scope || "user";
|
|
295
|
+
const supported = CLIENTS.filter((c) => c.supportsScope(scope));
|
|
296
|
+
if (opts.clients) return selectClients(opts.clients, scope, cwd);
|
|
297
|
+
const detected = new Set(supported.filter((c) => c.detect({ scope, cwd })));
|
|
298
|
+
if (opts.yes) return [...detected];
|
|
299
|
+
if (!interactive()) {
|
|
300
|
+
ui.warn("Not a terminal: pass --yes (every detected tool) or --client <ids>.");
|
|
301
|
+
return null;
|
|
155
302
|
}
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
303
|
+
// Installed tools first, then the rest, each in catalogue order.
|
|
304
|
+
const ordered = [...supported.filter((c) => detected.has(c)), ...supported.filter((c) => !detected.has(c))];
|
|
305
|
+
const picked = await pick(
|
|
306
|
+
[
|
|
307
|
+
...ordered.map((c) => ({ label: c.name, hint: detected.has(c) ? "" : "not installed", checked: detected.has(c), client: c })),
|
|
308
|
+
{
|
|
309
|
+
label: "Teach tools to use Memro automatically",
|
|
310
|
+
hint: "notes in CLAUDE.md, AGENTS.md…",
|
|
311
|
+
summary: "+ instructions",
|
|
312
|
+
checked: opts.instructions,
|
|
313
|
+
option: "instructions",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
{ title: scope === "project" ? "Which tools should use Memro in this project?" : "Which tools should use Memro?" },
|
|
317
|
+
);
|
|
318
|
+
if (!picked) return null;
|
|
319
|
+
opts.instructions = picked.some((p) => p.option === "instructions");
|
|
320
|
+
return picked.filter((p) => p.client).map((p) => p.client);
|
|
160
321
|
}
|
|
161
322
|
|
|
323
|
+
const interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
324
|
+
const host = (url) => url.replace(/^https?:\/\//, "");
|
|
325
|
+
|
|
162
326
|
async function connect(opts) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
327
|
+
ui.intro("Memro connect", host(opts.api));
|
|
328
|
+
ui.gap();
|
|
329
|
+
|
|
330
|
+
// Ask where to set up unless a flag already said so.
|
|
331
|
+
if (!opts.scopeExplicit && interactive() && !opts.yes && !opts.clients) {
|
|
332
|
+
const here = resolveProjectRoot(process.cwd());
|
|
333
|
+
const scopeChoice = await pickOne(
|
|
334
|
+
[
|
|
335
|
+
{ label: "All my projects", hint: "Adds Memro to your tools globally", summary: "All projects (global)", value: "user" },
|
|
336
|
+
{
|
|
337
|
+
label: "Only this project",
|
|
338
|
+
hint: isHomeOrRoot(here) ? "Run this inside a project folder to use it" : tilde(here),
|
|
339
|
+
summary: `This project · ${tilde(here)}`,
|
|
340
|
+
value: "project",
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
{ title: "Where should Memro be set up?" },
|
|
344
|
+
);
|
|
345
|
+
if (!scopeChoice) return ui.cancelled();
|
|
346
|
+
opts.scope = scopeChoice.value;
|
|
176
347
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
348
|
+
|
|
349
|
+
const scope = opts.scope || "user";
|
|
350
|
+
const cwd = scope === "project" ? resolveProjectRoot(process.cwd()) : process.cwd();
|
|
351
|
+
|
|
352
|
+
if (scope === "project") {
|
|
353
|
+
if (isHomeOrRoot(cwd)) {
|
|
354
|
+
ui.warn(`${tilde(cwd)} is not a project folder.`);
|
|
355
|
+
ui.line("Run this inside a project, or choose “All my projects”.");
|
|
356
|
+
return ui.outro("Nothing changed.");
|
|
357
|
+
}
|
|
358
|
+
if (opts.scopeExplicit) ui.answered("Project", tilde(cwd));
|
|
359
|
+
|
|
360
|
+
// Ask about memory isolation if no key was passed and terminal is interactive
|
|
361
|
+
if (!opts.key && !process.env.MEMRO_API_KEY && interactive() && !opts.yes) {
|
|
362
|
+
const memoryChoice = await pickOne(
|
|
363
|
+
[
|
|
364
|
+
{
|
|
365
|
+
label: "Shared with my account",
|
|
366
|
+
hint: "Same memory as your other projects and tools",
|
|
367
|
+
summary: "Shared with your account",
|
|
368
|
+
value: "shared",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
label: "Separate for this project",
|
|
372
|
+
hint: "Use a dedicated API key; only this project sees its memory",
|
|
373
|
+
summary: "Separate (project API key)",
|
|
374
|
+
value: "isolated",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
{ title: "Which memory should this project use?" },
|
|
378
|
+
);
|
|
379
|
+
if (!memoryChoice) return ui.cancelled();
|
|
380
|
+
if (memoryChoice.value === "isolated") {
|
|
381
|
+
const enteredKey = await promptText(`${ui.bar()} Project API key (memro_sk_…): `);
|
|
382
|
+
if (!enteredKey) return ui.cancelled("No key entered. Nothing changed.");
|
|
383
|
+
opts.key = enteredKey;
|
|
384
|
+
ui.gap();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
181
387
|
}
|
|
182
388
|
|
|
389
|
+
// Choose first, so cancelling never creates an API key.
|
|
390
|
+
const targets = await chooseTargets(opts, cwd);
|
|
391
|
+
if (!targets) return ui.cancelled();
|
|
392
|
+
if (targets.length === 0 && !opts.yes) return ui.cancelled("No tools selected. Nothing changed.");
|
|
393
|
+
|
|
394
|
+
let creds = null;
|
|
395
|
+
const providedKey = opts.key || process.env.MEMRO_API_KEY;
|
|
396
|
+
if (providedKey) {
|
|
397
|
+
const who = await request(opts.api, "/auth/whoami", { key: providedKey });
|
|
398
|
+
if (!who.ok) {
|
|
399
|
+
throw new Error(`The provided API key is not valid (${who.status}) on ${opts.api}`);
|
|
400
|
+
}
|
|
401
|
+
const agentId = who.data?.agent_id || who.data?.id || "unknown";
|
|
402
|
+
const mcpUrl = opts.api === ENVIRONMENTS.prod ? "https://mcp.memro.co/mcp" : `${opts.api}/mcp`;
|
|
403
|
+
creds = { api_url: opts.api, mcp_url: mcpUrl, agent_id: agentId, api_key: providedKey };
|
|
404
|
+
ui.info(`Using the project key ${ui.c.dim(`(agent ${agentId.slice(0, 12)}…)`)}`);
|
|
405
|
+
} else {
|
|
406
|
+
creds = loadCredentials();
|
|
407
|
+
if (creds?.api_url === opts.api) {
|
|
408
|
+
const who = await request(opts.api, "/auth/whoami", { key: creds.api_key });
|
|
409
|
+
if (who.status === 401 || who.status === 403) creds = null;
|
|
410
|
+
// Anything else (an outage, a rate limit) says nothing about the key;
|
|
411
|
+
// signing in again would mint a second one.
|
|
412
|
+
else if (!who.ok) throw new Error(`Memro API returned ${who.status}. Try again in a moment.`);
|
|
413
|
+
else ui.info(`Signed in ${ui.c.dim(`(agent ${creds.agent_id.slice(0, 12)}…)`)}`);
|
|
414
|
+
} else {
|
|
415
|
+
creds = null;
|
|
416
|
+
}
|
|
417
|
+
if (!creds) {
|
|
418
|
+
const res = await deviceLogin(opts.api);
|
|
419
|
+
creds = { api_url: opts.api, mcp_url: res.mcp_url, agent_id: res.agent_id, api_key: res.api_key };
|
|
420
|
+
saveCredentials(creds);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
ui.gap();
|
|
424
|
+
|
|
183
425
|
if (targets.length === 0) {
|
|
184
|
-
|
|
426
|
+
ui.warn("No supported tools found for this setup.");
|
|
185
427
|
printManual(creds);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
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;
|
|
428
|
+
return ui.outro("Nothing else changed.");
|
|
192
429
|
}
|
|
193
430
|
|
|
431
|
+
ui.info(scope === "project" ? `Adding Memro to this project` : "Adding Memro to your tools");
|
|
194
432
|
const conn = { mcpUrl: creds.mcp_url, apiKey: creds.api_key };
|
|
433
|
+
const width = Math.max(...targets.map((c) => c.name.length)) + 2;
|
|
434
|
+
const gitTracked = scope === "project" && isGitTracked(cwd);
|
|
195
435
|
let ok = 0;
|
|
196
436
|
for (const c of targets) {
|
|
197
437
|
try {
|
|
198
|
-
c.add(conn);
|
|
438
|
+
c.add(conn, { scope, cwd });
|
|
199
439
|
ok++;
|
|
200
|
-
|
|
440
|
+
const configFile = c.file(scope, cwd);
|
|
441
|
+
let note = "";
|
|
442
|
+
if (gitTracked && ensureGitIgnored(configFile, cwd).added) note = "added to .gitignore (holds your key)";
|
|
443
|
+
ui.ok(c.name.padEnd(width), `${formatPath(configFile, scope, cwd)}${note ? " " + note : ""}`);
|
|
444
|
+
if (opts.instructions && c.rules) {
|
|
445
|
+
const rulesFile = c.rules(scope, cwd);
|
|
446
|
+
if (rulesFile) {
|
|
447
|
+
const result = installInstructions(rulesFile, { scope });
|
|
448
|
+
const what = result === "kept-own" ? "already mentions Memro" : "use Memro automatically";
|
|
449
|
+
ui.ok("".padEnd(width), `${formatPath(rulesFile, scope, cwd)} ${what}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
201
452
|
} catch (err) {
|
|
202
|
-
|
|
453
|
+
ui.fail(c.name.padEnd(width), err.message);
|
|
203
454
|
}
|
|
204
455
|
}
|
|
205
|
-
|
|
206
|
-
|
|
456
|
+
ui.gap();
|
|
457
|
+
ui.outro(
|
|
458
|
+
ok ? `Done. Restart ${ok === 1 ? "the tool" : "your tools"} to load Memro.` : "Nothing was configured.",
|
|
459
|
+
"Manage or revoke the key in the dashboard → API keys.",
|
|
460
|
+
);
|
|
207
461
|
}
|
|
208
462
|
|
|
209
463
|
function printManual(creds) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
464
|
+
ui.line("Add this MCP server to your tool by hand:");
|
|
465
|
+
ui.line(`URL ${creds.mcp_url}`);
|
|
466
|
+
ui.line("Header Authorization: Bearer <the key in ~/.memro/credentials.json>");
|
|
467
|
+
ui.gap();
|
|
213
468
|
}
|
|
214
469
|
|
|
215
470
|
async function status(opts) {
|
|
471
|
+
const scope = opts.scope || "user";
|
|
472
|
+
const cwd = scope === "project" ? resolveProjectRoot(process.cwd()) : process.cwd();
|
|
216
473
|
const creds = loadCredentials();
|
|
217
|
-
|
|
474
|
+
const activeKey = opts.key || process.env.MEMRO_API_KEY || creds?.api_key;
|
|
475
|
+
if (!creds && !activeKey) {
|
|
218
476
|
console.log("Not connected. Run `npx memro-cli connect`.");
|
|
219
477
|
return;
|
|
220
478
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
console.log(`
|
|
224
|
-
console.log(`
|
|
479
|
+
const apiUrl = creds?.api_url || opts.api;
|
|
480
|
+
const who = await request(apiUrl, "/auth/whoami", { key: activeKey });
|
|
481
|
+
console.log(`API: ${apiUrl}`);
|
|
482
|
+
console.log(`MCP: ${creds?.mcp_url || (apiUrl === ENVIRONMENTS.prod ? "https://mcp.memro.co/mcp" : `${apiUrl}/mcp`)}`);
|
|
483
|
+
console.log(`Agent: ${who.data?.agent_id || creds?.agent_id || "unknown"}`);
|
|
225
484
|
console.log(`Key: ${who.ok ? "valid" : `not valid (${who.status}) — run \`npx memro-cli connect\``}`);
|
|
226
|
-
|
|
485
|
+
console.log(`Scope: ${scope}`);
|
|
486
|
+
if (scope === "project") {
|
|
487
|
+
console.log(`Root: ${cwd}`);
|
|
488
|
+
}
|
|
489
|
+
const supported = CLIENTS.filter((c) => c.supportsScope(scope));
|
|
490
|
+
const configured = supported.filter((c) => {
|
|
227
491
|
try {
|
|
228
|
-
const
|
|
229
|
-
|
|
492
|
+
const targetFile = c.file(scope, cwd);
|
|
493
|
+
const text = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, "utf8") : "";
|
|
494
|
+
return text.includes("memro");
|
|
230
495
|
} catch { return false; }
|
|
231
496
|
});
|
|
232
497
|
console.log(`Tools: ${configured.map((c) => c.name).join(", ") || "none"}`);
|
|
233
|
-
void opts;
|
|
234
498
|
}
|
|
235
499
|
|
|
236
500
|
async function disconnect(opts) {
|
|
237
|
-
const
|
|
501
|
+
const scope = opts.scope || "user";
|
|
502
|
+
const cwd = scope === "project" ? resolveProjectRoot(process.cwd()) : process.cwd();
|
|
503
|
+
const targets = opts.clients ? selectClients(opts.clients, scope, cwd) : CLIENTS.filter((c) => c.supportsScope(scope));
|
|
238
504
|
for (const c of targets) {
|
|
239
505
|
try {
|
|
240
|
-
if (c.remove()) console.log(` ✔ Removed from ${c.name}`);
|
|
506
|
+
if (c.remove({ scope, cwd })) console.log(` ✔ Removed from ${c.name} (${scope})`);
|
|
507
|
+
if (c.rules) {
|
|
508
|
+
const rulesFile = c.rules(scope, cwd);
|
|
509
|
+
if (rulesFile && uninstallInstructions(rulesFile)) {
|
|
510
|
+
console.log(` ✔ Removed the Memro note from ${formatPath(rulesFile, scope, cwd)}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
241
513
|
} catch (err) {
|
|
242
514
|
console.log(` ! ${c.name}: ${err.message}`);
|
|
243
515
|
}
|
|
244
516
|
}
|
|
245
|
-
if (!opts.clients && fs.existsSync(CREDENTIALS())) {
|
|
517
|
+
if (scope === "user" && !opts.clients && fs.existsSync(CREDENTIALS())) {
|
|
246
518
|
fs.rmSync(CREDENTIALS());
|
|
247
519
|
console.log(" ✔ Forgot the local key. Revoke it in the dashboard → API keys.");
|
|
248
520
|
}
|
|
@@ -250,6 +522,14 @@ async function disconnect(opts) {
|
|
|
250
522
|
|
|
251
523
|
const tilde = (p) => (p.startsWith(os.homedir()) ? "~" + p.slice(os.homedir().length) : p);
|
|
252
524
|
|
|
525
|
+
function formatPath(filePath, scope, cwd) {
|
|
526
|
+
if (scope === "project") {
|
|
527
|
+
const rel = path.relative(cwd, filePath);
|
|
528
|
+
return rel.startsWith("..") || path.isAbsolute(rel) ? tilde(filePath) : `./${rel}`;
|
|
529
|
+
}
|
|
530
|
+
return tilde(filePath);
|
|
531
|
+
}
|
|
532
|
+
|
|
253
533
|
export async function main(argv) {
|
|
254
534
|
const opts = parseArgs(argv);
|
|
255
535
|
switch (opts.command) {
|