memro-cli 0.1.1 → 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 +73 -20
- package/package.json +2 -2
- package/src/cli.js +340 -75
- package/src/clients.js +152 -55
- package/src/instructions.js +109 -0
- package/src/picker.js +115 -33
- 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
5
|
|
|
6
6
|
import { CLIENTS, findClient } from "./clients.js";
|
|
7
|
-
import { pick } from "./picker.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",
|
|
@@ -24,28 +26,65 @@ Usage
|
|
|
24
26
|
npx memro-cli disconnect Remove Memro from your tools and forget the key
|
|
25
27
|
|
|
26
28
|
Options
|
|
27
|
-
--
|
|
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
|
|
28
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…)
|
|
29
36
|
--staging | --local Use staging or a local server instead of Memro Cloud
|
|
30
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,11 +179,108 @@ 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
|
}
|
|
@@ -153,111 +290,231 @@ function selectClients(ids) {
|
|
|
153
290
|
* otherwise the user picks in a list with detected tools pre-selected.
|
|
154
291
|
* Returns null when the user cancels or nothing can be asked.
|
|
155
292
|
*/
|
|
156
|
-
async function chooseTargets(opts) {
|
|
157
|
-
|
|
158
|
-
const
|
|
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 })));
|
|
159
298
|
if (opts.yes) return [...detected];
|
|
160
|
-
if (!
|
|
161
|
-
|
|
299
|
+
if (!interactive()) {
|
|
300
|
+
ui.warn("Not a terminal: pass --yes (every detected tool) or --client <ids>.");
|
|
162
301
|
return null;
|
|
163
302
|
}
|
|
164
|
-
|
|
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))];
|
|
165
305
|
const picked = await pick(
|
|
166
|
-
|
|
167
|
-
|
|
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?" },
|
|
168
317
|
);
|
|
169
|
-
|
|
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);
|
|
170
321
|
}
|
|
171
322
|
|
|
323
|
+
const interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
324
|
+
const host = (url) => url.replace(/^https?:\/\//, "");
|
|
325
|
+
|
|
172
326
|
async function connect(opts) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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;
|
|
179
347
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
387
|
}
|
|
184
388
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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)}…)`)}`);
|
|
193
405
|
} else {
|
|
194
|
-
creds =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
422
|
}
|
|
423
|
+
ui.gap();
|
|
201
424
|
|
|
202
425
|
if (targets.length === 0) {
|
|
203
|
-
|
|
426
|
+
ui.warn("No supported tools found for this setup.");
|
|
204
427
|
printManual(creds);
|
|
205
|
-
return;
|
|
428
|
+
return ui.outro("Nothing else changed.");
|
|
206
429
|
}
|
|
207
|
-
console.log(`\n Adding Memro to: ${targets.map((c) => c.name).join(", ")}`);
|
|
208
430
|
|
|
431
|
+
ui.info(scope === "project" ? `Adding Memro to this project` : "Adding Memro to your tools");
|
|
209
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);
|
|
210
435
|
let ok = 0;
|
|
211
436
|
for (const c of targets) {
|
|
212
437
|
try {
|
|
213
|
-
c.add(conn);
|
|
438
|
+
c.add(conn, { scope, cwd });
|
|
214
439
|
ok++;
|
|
215
|
-
|
|
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
|
+
}
|
|
216
452
|
} catch (err) {
|
|
217
|
-
|
|
453
|
+
ui.fail(c.name.padEnd(width), err.message);
|
|
218
454
|
}
|
|
219
455
|
}
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
);
|
|
222
461
|
}
|
|
223
462
|
|
|
224
463
|
function printManual(creds) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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();
|
|
228
468
|
}
|
|
229
469
|
|
|
230
470
|
async function status(opts) {
|
|
471
|
+
const scope = opts.scope || "user";
|
|
472
|
+
const cwd = scope === "project" ? resolveProjectRoot(process.cwd()) : process.cwd();
|
|
231
473
|
const creds = loadCredentials();
|
|
232
|
-
|
|
474
|
+
const activeKey = opts.key || process.env.MEMRO_API_KEY || creds?.api_key;
|
|
475
|
+
if (!creds && !activeKey) {
|
|
233
476
|
console.log("Not connected. Run `npx memro-cli connect`.");
|
|
234
477
|
return;
|
|
235
478
|
}
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
console.log(`
|
|
239
|
-
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"}`);
|
|
240
484
|
console.log(`Key: ${who.ok ? "valid" : `not valid (${who.status}) — run \`npx memro-cli connect\``}`);
|
|
241
|
-
|
|
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) => {
|
|
242
491
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
492
|
+
const targetFile = c.file(scope, cwd);
|
|
493
|
+
const text = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, "utf8") : "";
|
|
494
|
+
return text.includes("memro");
|
|
245
495
|
} catch { return false; }
|
|
246
496
|
});
|
|
247
497
|
console.log(`Tools: ${configured.map((c) => c.name).join(", ") || "none"}`);
|
|
248
|
-
void opts;
|
|
249
498
|
}
|
|
250
499
|
|
|
251
500
|
async function disconnect(opts) {
|
|
252
|
-
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));
|
|
253
504
|
for (const c of targets) {
|
|
254
505
|
try {
|
|
255
|
-
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
|
+
}
|
|
256
513
|
} catch (err) {
|
|
257
514
|
console.log(` ! ${c.name}: ${err.message}`);
|
|
258
515
|
}
|
|
259
516
|
}
|
|
260
|
-
if (!opts.clients && fs.existsSync(CREDENTIALS())) {
|
|
517
|
+
if (scope === "user" && !opts.clients && fs.existsSync(CREDENTIALS())) {
|
|
261
518
|
fs.rmSync(CREDENTIALS());
|
|
262
519
|
console.log(" ✔ Forgot the local key. Revoke it in the dashboard → API keys.");
|
|
263
520
|
}
|
|
@@ -265,6 +522,14 @@ async function disconnect(opts) {
|
|
|
265
522
|
|
|
266
523
|
const tilde = (p) => (p.startsWith(os.homedir()) ? "~" + p.slice(os.homedir().length) : p);
|
|
267
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
|
+
|
|
268
533
|
export async function main(argv) {
|
|
269
534
|
const opts = parseArgs(argv);
|
|
270
535
|
switch (opts.command) {
|