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/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
- Antigravity, OpenCode, Gemini CLI, VS Code, Windsurf
22
- and Claude Desktop
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
- --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)
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") opts.clients = (argv[++i] || "").split(",").map((s) => s.trim()).filter(Boolean);
38
- else if (a === "--yes" || a === "-y") opts.yes = true;
39
- else if (a === "--staging") opts.api = ENVIRONMENTS.staging;
40
- else if (a === "--local") opts.api = ENVIRONMENTS.local;
41
- else if (a === "--api") {
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
- console.log(`\n Your code: \x1b[1m${user_code}\x1b[0m`);
99
- console.log(` Opening ${verification_uri_complete}`);
100
- console.log(" Sign in there and press Connect. (If the browser didn't open, visit the link.)\n");
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(" Waiting for approval…");
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(" approved.\n");
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 selectClients(ids) {
143
- if (!ids) return CLIENTS.filter((c) => c.detect());
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
- 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;
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
- 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";
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
- const targets = selectClients(opts.clients);
164
- console.log(`\n◆ Memro connect (${opts.api})`);
165
-
166
- let creds = loadCredentials();
167
- if (creds?.api_url === opts.api) {
168
- const who = await request(opts.api, "/auth/whoami", { key: creds.api_key });
169
- if (who.status === 401 || who.status === 403) creds = null;
170
- // Anything else (an outage, a rate limit) says nothing about the key;
171
- // signing in again would mint a second one.
172
- else if (!who.ok) throw new Error(`Memro API returned ${who.status}. Try again in a moment.`);
173
- else console.log(` Already signed in as agent ${creds.agent_id.slice(0, 12)}…`);
174
- } else {
175
- creds = null;
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
- if (!creds) {
178
- const res = await deviceLogin(opts.api);
179
- creds = { api_url: opts.api, mcp_url: res.mcp_url, agent_id: res.agent_id, api_key: res.api_key };
180
- saveCredentials(creds);
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
- console.log("\n No supported tools found on this machine.");
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
- console.log(` \x1b[32m✔\x1b[0m ${c.name.padEnd(15)} ${tilde(c.file())}`);
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
- console.log(` \x1b[33m!\x1b[0m ${c.name.padEnd(15)} ${err.message}`);
453
+ ui.fail(c.name.padEnd(width), err.message);
203
454
  }
204
455
  }
205
- console.log(`\n Done. Restart ${ok === 1 ? "the tool" : "your tools"} to load Memro's memory tools.`);
206
- console.log(" Manage or revoke the key anytime in the dashboard → API keys.\n");
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
- console.log("\n Add this MCP server to your tool manually:");
211
- console.log(` URL: ${creds.mcp_url}`);
212
- console.log(" Header: Authorization: Bearer <the key in ~/.memro/credentials.json>\n");
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
- if (!creds) {
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 who = await request(creds.api_url, "/auth/whoami", { key: creds.api_key });
222
- console.log(`API: ${creds.api_url}`);
223
- console.log(`MCP: ${creds.mcp_url}`);
224
- console.log(`Agent: ${creds.agent_id}`);
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
- const configured = CLIENTS.filter((c) => {
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 text = fs.existsSync(c.file()) ? fs.readFileSync(c.file(), "utf8") : "";
229
- return text.includes(creds.mcp_url);
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 targets = opts.clients ? selectClients(opts.clients) : CLIENTS;
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) {