@enfyra/mcp-server 0.0.27 → 0.0.28

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
@@ -17,7 +17,7 @@ From your **Enfyra project root**:
17
17
  npx @enfyra/mcp-server config
18
18
  ```
19
19
 
20
- - **Interactive (default in a terminal):** first asks **where** to write config — `[1]` Claude Code, `[2]` Cursor, `[3]` Codex, `[4]` all (default) — unless you already passed target flags. Then prompts for `ENFYRA_API_URL`, `ENFYRA_EMAIL`, and `ENFYRA_PASSWORD` when missing. Press **Enter** to accept bracketed defaults from env or existing `enfyra` config. Password **Enter** keeps the current saved password when updating.
20
+ - **Interactive (default in a terminal):** first asks **where** to write config with an arrow-key selector — Claude Code, Cursor, Codex, or all — unless you already passed target flags. Then prompts for `ENFYRA_API_URL`, `ENFYRA_EMAIL`, and `ENFYRA_PASSWORD` when missing. Press **Enter** to accept bracketed defaults from env or existing `enfyra` config. Password **Enter** keeps the current saved password when updating.
21
21
  - **Re-run anytime** to update the same files; other entries under `mcpServers` are preserved.
22
22
  - **Non-interactive** (CI / scripts): `npx @enfyra/mcp-server config --yes` plus optional `-a` / `-e` / `-p` and/or env vars.
23
23
  - **One host only:** `--claude-code` / `--claude` / `--claude-only` → `./.mcp.json`. `--cursor` / `--cursor-only` → `./.cursor/mcp.json`. `--codex` / `--codex-only` → `~/.codex/config.toml`. Pass multiple target flags to write each selected host.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { createInterface } from 'node:readline/promises';
3
+ import { emitKeypressEvents } from 'node:readline';
3
4
  import { stdin as input, stdout as output, cwd } from 'node:process';
4
5
  import { dirname, join } from 'node:path';
5
6
  import { homedir } from 'node:os';
@@ -23,14 +24,14 @@ Options:
23
24
  --password, -p <secret> ENFYRA_PASSWORD
24
25
  --reconfig Always choose target again in interactive mode and replace the old enfyra config for that target
25
26
  --yes Non-interactive: no prompts (CI / scripts); use CLI, env, existing file, then defaults
26
- Target — non-interactive default is all; with TTY and no target flags, you are prompted [1]/[2]/[3]/[4]:
27
+ Target — non-interactive default is all; with TTY and no target flags, choose with ↑/↓:
27
28
  --claude-code, --claude, --claude-only Only ./.mcp.json (Claude Code project scope)
28
29
  --cursor, --cursor-only Only ./.cursor/mcp.json (Cursor)
29
30
  --codex, --codex-only Only ~/.codex/config.toml (Codex)
30
31
  Passing multiple target flags writes each selected target.
31
32
  -h, --help Show this help
32
33
 
33
- Interactive mode: asks Claude Code vs Cursor vs both if you did not pass target flags; then asks for URL / email / password
34
+ Interactive mode: lets you choose Claude Code / Cursor / Codex / all if you did not pass target flags; then asks for URL / email / password
34
35
  when missing. Existing ./.mcp.json, ./.cursor/mcp.json, and ~/.codex/config.toml are used as defaults. Re-run to update.
35
36
 
36
37
  Examples:
@@ -231,6 +232,28 @@ async function loadExistingEnfyraEnv(root, readClaude, readCursor, readCodex) {
231
232
  }
232
233
 
233
234
  async function promptTargetChoice() {
235
+ const choices = [
236
+ {
237
+ label: 'Claude Code — ./.mcp.json',
238
+ value: { claude: true, cursor: false, codex: false },
239
+ },
240
+ {
241
+ label: 'Cursor — ./.cursor/mcp.json',
242
+ value: { claude: false, cursor: true, codex: false },
243
+ },
244
+ {
245
+ label: 'Codex — ~/.codex/config.toml',
246
+ value: { claude: false, cursor: false, codex: true },
247
+ },
248
+ {
249
+ label: 'All',
250
+ value: { claude: true, cursor: true, codex: true },
251
+ },
252
+ ];
253
+ if (input.setRawMode && output.isTTY) {
254
+ return promptTargetSelect(choices, 3);
255
+ }
256
+
234
257
  const rl = createInterface({ input, output });
235
258
  const line = (await rl.question(
236
259
  'Where should Enfyra MCP config be written?\n'
@@ -256,6 +279,66 @@ async function promptTargetChoice() {
256
279
  return { claude: true, cursor: true, codex: true };
257
280
  }
258
281
 
282
+ async function promptTargetSelect(choices, initialIndex = 0) {
283
+ let selected = Math.max(0, Math.min(initialIndex, choices.length - 1));
284
+ let renderedLines = 0;
285
+
286
+ const render = () => {
287
+ if (renderedLines > 0) {
288
+ output.write(`\x1B[${renderedLines}A\x1B[0J`);
289
+ }
290
+ const lines = [
291
+ 'Where should Enfyra MCP config be written?',
292
+ ...choices.map((choice, index) => `${index === selected ? '›' : ' '} ${choice.label}`),
293
+ '',
294
+ 'Use ↑/↓ to move, Enter to select.',
295
+ ];
296
+ renderedLines = lines.length;
297
+ output.write(`${lines.join('\n')}\n`);
298
+ };
299
+
300
+ return new Promise((resolve, reject) => {
301
+ const wasRaw = input.isRaw;
302
+ const cleanup = () => {
303
+ input.off('keypress', onKeypress);
304
+ if (!wasRaw) input.setRawMode(false);
305
+ output.write('\x1B[?25h');
306
+ };
307
+ const finish = () => {
308
+ cleanup();
309
+ resolve(choices[selected].value);
310
+ };
311
+ const onKeypress = (_str, key = {}) => {
312
+ if (key.ctrl && key.name === 'c') {
313
+ cleanup();
314
+ output.write('\n');
315
+ reject(new Error('Cancelled'));
316
+ return;
317
+ }
318
+ if (key.name === 'up') {
319
+ selected = selected <= 0 ? choices.length - 1 : selected - 1;
320
+ render();
321
+ return;
322
+ }
323
+ if (key.name === 'down') {
324
+ selected = selected >= choices.length - 1 ? 0 : selected + 1;
325
+ render();
326
+ return;
327
+ }
328
+ if (key.name === 'return' || key.name === 'enter') {
329
+ finish();
330
+ }
331
+ };
332
+
333
+ emitKeypressEvents(input);
334
+ input.setRawMode(true);
335
+ input.resume();
336
+ output.write('\x1B[?25l');
337
+ input.on('keypress', onKeypress);
338
+ render();
339
+ });
340
+ }
341
+
259
342
  async function promptConfig(opts, existing) {
260
343
  let apiUrl = opts.apiUrl;
261
344
  let email = opts.email;