@niroai/niro 0.9.0 → 0.10.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/src/mcp/setup.js CHANGED
@@ -163,16 +163,16 @@ function writeManagedSkill(absPath, content, label) {
163
163
  if (!fs.existsSync(absPath)) {
164
164
  fs.mkdirSync(path.dirname(absPath), { recursive: true });
165
165
  fs.writeFileSync(absPath, content);
166
- console.log(` ${label} added: ${absPath}`);
166
+ console.log(` ${label} added: ${absPath}`);
167
167
  return "added";
168
168
  }
169
169
  const existing = fs.readFileSync(absPath, "utf-8");
170
170
  if (existing === content) {
171
- console.log(` ${label} already up to date — skipping (${absPath}).`);
171
+ console.log(` ${label} already up to date. Skipping (${absPath}).`);
172
172
  return "unchanged";
173
173
  }
174
174
  fs.writeFileSync(absPath, content);
175
- console.log(` ${label} updated: ${absPath}`);
175
+ console.log(` ${label} updated: ${absPath}`);
176
176
  return "updated";
177
177
  }
178
178
 
@@ -355,7 +355,7 @@ function ensureCursorPermissionsForNiro() {
355
355
  if (!data || typeof data !== "object") data = {};
356
356
  }
357
357
  } catch (e) {
358
- console.warn(` Could not read ${permPath}: ${e.message}`);
358
+ console.warn(` Could not read ${permPath}: ${e.message}`);
359
359
  console.warn(' Fix the file manually or merge: { "mcpAllowlist": ["niro:*"] }\n');
360
360
  return;
361
361
  }
@@ -366,21 +366,21 @@ function ensureCursorPermissionsForNiro() {
366
366
  data = { ...data, mcpAllowlist: ["niro:*"] };
367
367
  } else if (hasKey && Array.isArray(data.mcpAllowlist)) {
368
368
  if (allowlistAlreadyCoversAllNiroTools(data.mcpAllowlist)) {
369
- console.log(" Cursor MCP allowlist already includes Niro (`niro:*` or broader).\n");
369
+ console.log(" Cursor MCP allowlist already includes Niro (`niro:*` or broader).\n");
370
370
  return;
371
371
  }
372
372
  data.mcpAllowlist = [...data.mcpAllowlist, "niro:*"];
373
373
  } else if (hasKey && !Array.isArray(data.mcpAllowlist)) {
374
- console.warn(" ~/.cursor/permissions.json has invalid mcpAllowlist — not modifying it.");
374
+ console.warn(" ~/.cursor/permissions.json has invalid mcpAllowlist, so it is not being modified.");
375
375
  console.warn(' Add this string after fixing JSON: \"niro:*\"\n');
376
376
  return;
377
377
  } else if (Object.keys(data).length > 0) {
378
378
  console.log("");
379
- console.log(" ~/.cursor/permissions.json exists but has no \"mcpAllowlist\" entry.");
379
+ console.log(" ~/.cursor/permissions.json exists but has no \"mcpAllowlist\" entry.");
380
380
  console.log(
381
- ' Cursor replaces the Settings MCP allowlist when you add \"mcpAllowlist\" — skipped auto-merge to avoid dropping other MCP tools.',
381
+ ' Cursor replaces the Settings MCP allowlist when you add \"mcpAllowlist\", so the auto-merge was skipped to avoid dropping other MCP tools.',
382
382
  );
383
- console.log(" Merge manually (include other MCP servers if you need them), e.g.:");
383
+ console.log(" Merge manually (include other MCP servers if you need them), e.g.:");
384
384
  console.log(' \"mcpAllowlist\": [\"niro:*\", \"github:*\"]\n');
385
385
  return;
386
386
  } else {
@@ -388,19 +388,19 @@ function ensureCursorPermissionsForNiro() {
388
388
  }
389
389
 
390
390
  if (!data.mcpAllowlist || !allowlistAlreadyCoversAllNiroTools(data.mcpAllowlist)) {
391
- console.warn(" [niro-setup] Unexpected state writing permissions.json; skipping.\n");
391
+ console.warn(" [niro-setup] Unexpected state writing permissions.json; skipping.\n");
392
392
  return;
393
393
  }
394
394
 
395
395
  try {
396
396
  fs.mkdirSync(path.dirname(permPath), { recursive: true });
397
397
  fs.writeFileSync(permPath, JSON.stringify(data, null, 2) + "\n");
398
- console.log(` Cursor MCP permissions updated: ${permPath}`);
398
+ console.log(` Cursor MCP permissions updated: ${permPath}`);
399
399
  console.log(' • Added pattern \"niro:*\" so Agent can run all Niro tools without approval prompts.');
400
- console.log(" • Enable Cursor Agent Auto-run / sandbox auto-run — \"Ask every time\" ignores allowlists.");
401
- console.log(" • Reference: https://cursor.com/docs/reference/permissions\n");
400
+ console.log(" • Enable Cursor Agent Auto-run / sandbox auto-run: \"Ask every time\" ignores allowlists.");
401
+ console.log(" • Reference: https://cursor.com/docs/reference/permissions\n");
402
402
  } catch (e) {
403
- console.warn(` Could not write ${permPath}: ${e.message}\n`);
403
+ console.warn(` Could not write ${permPath}: ${e.message}\n`);
404
404
  }
405
405
  }
406
406
 
@@ -534,21 +534,52 @@ function hasShellMetacharacters(value) {
534
534
 
535
535
  function printHeader() {
536
536
  console.log("");
537
- console.log(" Niro MCP Server Setup");
538
- console.log(" ---------------------");
537
+ console.log(" Niro MCP Server Setup");
538
+ console.log(" ---------------------");
539
539
  console.log("");
540
540
  }
541
541
 
542
- function printClients() {
543
- console.log(" Supported clients:");
544
- Object.entries(CLIENTS).forEach(([id, client]) => {
545
- console.log(` ${id.padEnd(18)} ${client.name} (${client.description})`);
542
+ /** How many clients to name before summarising the rest. */
543
+ const CLIENTS_SHOWN = 5;
544
+
545
+ /**
546
+ * Run a client's own CLI and print its output at our indent level.
547
+ *
548
+ * These tools print at column 0, so inheriting their stdout put unindented lines in the
549
+ * middle of an indented block and destroyed the only cue for what belongs to what. On
550
+ * failure the output is printed as-is: a broken command is not the moment to be tidy.
551
+ */
552
+ function runClientCommand(command) {
553
+ const out = execSync(command, { stdio: ["inherit", "pipe", "pipe"], encoding: "utf8" });
554
+ String(out || "")
555
+ .split("\n")
556
+ .filter((l) => l.trim())
557
+ .forEach((l) => console.log(` ${l.trim()}`));
558
+ }
559
+
560
+ /**
561
+ * The first few by name, then a count.
562
+ *
563
+ * All fourteen used to print, with each one's config-file path in brackets: sixteen lines for
564
+ * a question nearly everyone answers with the default. The full list is one flag away, and
565
+ * anyone hunting for a client they cannot see is exactly the person who will use it.
566
+ */
567
+ function printClients({ all = false } = {}) {
568
+ const entries = Object.entries(CLIENTS);
569
+ const shown = all ? entries : entries.slice(0, CLIENTS_SHOWN);
570
+ console.log(all ? " All supported clients:" : " Clients:");
571
+ shown.forEach(([id, client]) => {
572
+ console.log(` ${id.padEnd(18)} ${client.name}${all ? ` (${client.description})` : ""}`);
546
573
  });
574
+ const rest = entries.length - shown.length;
575
+ if (rest > 0) {
576
+ console.log(` ...and ${rest} more. See them all with: niro mcp install --list`);
577
+ }
547
578
  console.log("");
548
579
  }
549
580
 
550
581
  async function setupClaudeCode(apiKey, mcpServerUrl) {
551
- console.log("\n Configuring Claude Code...\n");
582
+ console.log("\n Configuring Claude Code...\n");
552
583
 
553
584
  const cmd = [
554
585
  "claude", "mcp", "add",
@@ -563,12 +594,12 @@ async function setupClaudeCode(apiKey, mcpServerUrl) {
563
594
  cmd.push("--", "niro", "mcp", "serve");
564
595
 
565
596
  try {
566
- execSync(cmd.join(" "), { stdio: "inherit" });
567
- console.log(" MCP server registered globally in Claude Code.\n");
597
+ runClientCommand(cmd.join(" "));
598
+ console.log(" MCP server registered globally in Claude Code.\n");
568
599
  } catch (err) {
569
- console.error(" Failed to register MCP server. Is Claude Code installed?");
570
- console.error(" Install: https://claude.ai/code\n");
571
- console.error(` Manual command:\n ${cmd.join(" ")}\n`);
600
+ console.error(" Failed to register MCP server. Is Claude Code installed?");
601
+ console.error(" Install: https://claude.ai/code\n");
602
+ console.error(` Manual command:\n ${cmd.join(" ")}\n`);
572
603
  return false;
573
604
  }
574
605
 
@@ -608,7 +639,7 @@ function addClaudeCodePostCompactHook() {
608
639
  const alreadyPresent = settings.hooks.PostCompact.some(isNiroPostCompactHook);
609
640
 
610
641
  if (alreadyPresent) {
611
- console.log(" PostCompact hook already configured — skipping.\n");
642
+ console.log(" PostCompact hook already configured. Skipping.\n");
612
643
  return;
613
644
  }
614
645
 
@@ -621,11 +652,10 @@ function addClaudeCodePostCompactHook() {
621
652
  try {
622
653
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
623
654
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
624
- console.log(" PostCompact hook added to ~/.claude/settings.json\n");
625
- console.log(" → Claude will be reminded about Niro tools after context compaction.\n");
655
+ console.log(" PostCompact hook added to ~/.claude/settings.json\n");
626
656
  } catch (err) {
627
- console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
628
- console.warn(" Add manually to the PostCompact array in ~/.claude/settings.json:\n");
657
+ console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
658
+ console.warn(" Add manually to the PostCompact array in ~/.claude/settings.json:\n");
629
659
  console.warn(JSON.stringify(exampleEntry, null, 2) + "\n");
630
660
  }
631
661
  }
@@ -649,18 +679,17 @@ function addClaudeCodeMemory() {
649
679
 
650
680
  const { content, action } = upsertNiroMarkerSection(existing, section);
651
681
  if (action === "unchanged") {
652
- console.log(" CLAUDE.md entry already up to date — skipping.\n");
682
+ console.log(" CLAUDE.md entry already up to date. Skipping.\n");
653
683
  return;
654
684
  }
655
685
 
656
686
  try {
657
687
  fs.mkdirSync(path.dirname(claudeMdPath), { recursive: true });
658
688
  fs.writeFileSync(claudeMdPath, content);
659
- console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ~/.claude/CLAUDE.md\n`);
660
- console.log(" → Claude will see Niro tool guidance in every session.\n");
689
+ console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ~/.claude/CLAUDE.md\n`);
661
690
  } catch (err) {
662
- console.warn(` Could not write ~/.claude/CLAUDE.md: ${err.message}`);
663
- console.warn(" Add manually to ~/.claude/CLAUDE.md:\n");
691
+ console.warn(` Could not write ~/.claude/CLAUDE.md: ${err.message}`);
692
+ console.warn(" Add manually to ~/.claude/CLAUDE.md:\n");
664
693
  console.warn(section + "\n");
665
694
  }
666
695
  }
@@ -718,7 +747,7 @@ function addClaudeCodeUserPromptHook() {
718
747
  (h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === hookCommand),
719
748
  );
720
749
  if (alreadyCurrent) {
721
- console.log(" UserPromptSubmit hook already up to date — skipping.\n");
750
+ console.log(" UserPromptSubmit hook already up to date. Skipping.\n");
722
751
  return;
723
752
  }
724
753
 
@@ -732,11 +761,9 @@ function addClaudeCodeUserPromptHook() {
732
761
  try {
733
762
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
734
763
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
735
- console.log(` UserPromptSubmit hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
736
- console.log(" → Claude is reminded about Niro before each prompt, but only in repos");
737
- console.log(" that have a .niro-project marker (no nagging in un-indexed folders).\n");
764
+ console.log(` UserPromptSubmit hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
738
765
  } catch (err) {
739
- console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
766
+ console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
740
767
  console.warn(' Add manually to the UserPromptSubmit array in ~/.claude/settings.json:\n');
741
768
  console.warn(JSON.stringify({ hooks: [{ type: "command", command: hookCommand }] }, null, 2) + "\n");
742
769
  }
@@ -815,10 +842,10 @@ function removeStaleNiroStopHook() {
815
842
 
816
843
  try {
817
844
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
818
- console.log(" Removed the old Stop hook — the end-of-task line now comes from Niro directly,");
819
- console.log(" without the \"Stop says:\" prefix Claude Code adds to hook output.\n");
845
+ console.log(" Removed the old Stop hook. The end-of-task line now comes from Niro directly,");
846
+ console.log(" without the \"Stop says:\" prefix Claude Code adds to hook output.\n");
820
847
  } catch (err) {
821
- console.warn(` Could not update ~/.claude/settings.json: ${err.message}`);
848
+ console.warn(` Could not update ~/.claude/settings.json: ${err.message}`);
822
849
  }
823
850
  }
824
851
 
@@ -844,7 +871,7 @@ function addClaudeCodePreToolUseHook() {
844
871
  (h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === NIRO_PRETOOL_COMMAND),
845
872
  );
846
873
  if (alreadyCurrent) {
847
- console.log(" PreToolUse temporary project hook already up to date — skipping.\n");
874
+ console.log(" PreToolUse temporary project hook already up to date. Skipping.\n");
848
875
  return;
849
876
  }
850
877
 
@@ -855,12 +882,10 @@ function addClaudeCodePreToolUseHook() {
855
882
  try {
856
883
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
857
884
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
858
- console.log(` PreToolUse temporary project hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
859
- console.log(" → Before editing a Niro-indexed repo, Claude is reminded once/session to run the");
860
- console.log(" should_create_temp_project check (non-blocking; silent in un-indexed repos).\n");
885
+ console.log(` PreToolUse temporary project hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
861
886
  } catch (err) {
862
- console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
863
- console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
887
+ console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
888
+ console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
864
889
  console.warn(JSON.stringify(entry, null, 2) + "\n");
865
890
  }
866
891
  }
@@ -945,7 +970,7 @@ function addClaudeCodeExploreHook() {
945
970
  (h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === NIRO_EXPLORE_COMMAND),
946
971
  );
947
972
  if (alreadyCurrent) {
948
- console.log(" PreToolUse explore-first hook already up to date — skipping.\n");
973
+ console.log(" PreToolUse explore-first hook already up to date. Skipping.\n");
949
974
  return;
950
975
  }
951
976
 
@@ -956,12 +981,10 @@ function addClaudeCodeExploreHook() {
956
981
  try {
957
982
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
958
983
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
959
- console.log(` PreToolUse explore-first hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
960
- console.log(" → On every native Grep/Glob/Read in a niro-indexed repo, Claude is reminded to prefer");
961
- console.log(" niro tools — even mid-task and late in a long session, not just for discovery questions.\n");
984
+ console.log(` PreToolUse explore-first hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
962
985
  } catch (err) {
963
- console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
964
- console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
986
+ console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
987
+ console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
965
988
  console.warn(JSON.stringify(entry, null, 2) + "\n");
966
989
  }
967
990
  }
@@ -1348,11 +1371,10 @@ function addClaudeCodeSkill() {
1348
1371
  try {
1349
1372
  const result = writeManagedSkill(skillPath, NIRO_SKILL, "niro skill");
1350
1373
  if (result === "added" || result === "updated") {
1351
- console.log(" → Claude loads the full Niro tool-routing playbook on demand.");
1352
1374
  }
1353
1375
  console.log("");
1354
1376
  } catch (err) {
1355
- console.warn(` Could not write ~/.claude/skills/niro-skill/SKILL.md: ${err.message}\n`);
1377
+ console.warn(` Could not write ~/.claude/skills/niro-skill/SKILL.md: ${err.message}\n`);
1356
1378
  }
1357
1379
  }
1358
1380
 
@@ -1366,11 +1388,10 @@ function addClaudeCodeCliSkill() {
1366
1388
  try {
1367
1389
  const result = writeManagedSkill(skillPath, NIRO_CLI_SKILL, "niro-cli skill");
1368
1390
  if (result === "added" || result === "updated") {
1369
- console.log(" → Claude can drive the niro CLI from chat (status / onboard / edit / rebuild / remove).");
1370
1391
  }
1371
1392
  console.log("");
1372
1393
  } catch (err) {
1373
- console.warn(` Could not write ~/.claude/skills/niro-cli/SKILL.md: ${err.message}\n`);
1394
+ console.warn(` Could not write ~/.claude/skills/niro-cli/SKILL.md: ${err.message}\n`);
1374
1395
  }
1375
1396
  }
1376
1397
 
@@ -1432,16 +1453,16 @@ via MCP tools.
1432
1453
  function ensureCursorRule() {
1433
1454
  const rulePath = path.join(process.cwd(), ".cursor", "rules", "niro.mdc");
1434
1455
  if (fs.existsSync(rulePath)) {
1435
- console.log(" Cursor rule already present — skipping (.cursor/rules/niro.mdc).\n");
1456
+ console.log(" Cursor rule already present. Skipping (.cursor/rules/niro.mdc).\n");
1436
1457
  return;
1437
1458
  }
1438
1459
  try {
1439
1460
  fs.mkdirSync(path.dirname(rulePath), { recursive: true });
1440
1461
  fs.writeFileSync(rulePath, NIRO_CURSOR_RULE);
1441
- console.log(` Cursor rule added: ${rulePath}`);
1442
- console.log(" • Project-scoped. For other repos, copy it or re-run setup there.\n");
1462
+ console.log(` Cursor rule added: ${rulePath}`);
1463
+ console.log(" • Project-scoped. For other repos, copy it or re-run setup there.\n");
1443
1464
  } catch (err) {
1444
- console.warn(` Could not write .cursor/rules/niro.mdc: ${err.message}\n`);
1465
+ console.warn(` Could not write .cursor/rules/niro.mdc: ${err.message}\n`);
1445
1466
  }
1446
1467
  }
1447
1468
 
@@ -1450,7 +1471,7 @@ function ensureCursorRule() {
1450
1471
  * the [mcp_servers.niro] table into ~/.codex/config.toml. Mirrors setupClaudeCode.
1451
1472
  */
1452
1473
  async function setupCodex(apiKey, mcpServerUrl) {
1453
- console.log("\n Configuring Codex...\n");
1474
+ console.log("\n Configuring Codex...\n");
1454
1475
 
1455
1476
  const cmd = [
1456
1477
  "codex", "mcp", "add",
@@ -1463,11 +1484,11 @@ async function setupCodex(apiKey, mcpServerUrl) {
1463
1484
 
1464
1485
  try {
1465
1486
  execSync(cmd.join(" "), { stdio: "inherit" });
1466
- console.log(" MCP server registered in Codex (~/.codex/config.toml).\n");
1487
+ console.log(" MCP server registered in Codex (~/.codex/config.toml).\n");
1467
1488
  } catch (err) {
1468
- console.error(" Failed to register MCP server. Is the Codex CLI installed?");
1469
- console.error(" Install: https://developers.openai.com/codex\n");
1470
- console.error(` Manual command:\n ${cmd.join(" ")}\n`);
1489
+ console.error(" Failed to register MCP server. Is the Codex CLI installed?");
1490
+ console.error(" Install: https://developers.openai.com/codex\n");
1491
+ console.error(` Manual command:\n ${cmd.join(" ")}\n`);
1471
1492
  return false;
1472
1493
  }
1473
1494
 
@@ -1574,19 +1595,19 @@ function clearCodexPerToolApprovals(cfgPath) {
1574
1595
  // Refuse to write anything that lost the niro server itself. Cheap, and the failure it guards
1575
1596
  // against is the one that matters.
1576
1597
  if (!rewritten.includes("[mcp_servers.niro]")) {
1577
- console.warn(" Skipped clearing Codex tool approvals — the edit would have dropped [mcp_servers.niro].\n");
1598
+ console.warn(" Skipped clearing Codex tool approvals: the edit would have dropped [mcp_servers.niro].\n");
1578
1599
  return 0;
1579
1600
  }
1580
1601
 
1581
1602
  try {
1582
1603
  writeCodexConfigVerified(cfgPath, rewritten);
1583
1604
  } catch (err) {
1584
- console.warn(` Could not clear Codex per-tool approvals: ${err.message}\n`);
1605
+ console.warn(` Could not clear Codex per-tool approvals: ${err.message}\n`);
1585
1606
  return 0;
1586
1607
  }
1587
1608
 
1588
- console.log(` Cleared ${matches.length} per-tool approval override${matches.length === 1 ? "" : "s"} for niro.`);
1589
- console.log(" → those beat default_tools_approval_mode and made every call prompt.\n");
1609
+ console.log(` Cleared ${matches.length} per-tool approval override${matches.length === 1 ? "" : "s"} for niro.`);
1610
+ console.log(" → those beat default_tools_approval_mode and made every call prompt.\n");
1590
1611
  return matches.length;
1591
1612
  }
1592
1613
 
@@ -1612,12 +1633,12 @@ function codexConfigPath() {
1612
1633
  function hardenCodexServerConfig() {
1613
1634
  const cfgPath = codexConfigPath();
1614
1635
  if (!cfgPath) {
1615
- console.warn(" Could not resolve ~/.codex/config.toml — skipping approval-mode hardening.\n");
1636
+ console.warn(" Could not resolve ~/.codex/config.toml. Skipping approval-mode hardening.\n");
1616
1637
  return;
1617
1638
  }
1618
1639
 
1619
- console.log(" Tip: for a hard dependency, add `required = true` under [mcp_servers.niro]");
1620
- console.log(" (Codex then fails startup if niro is unreachable — off by default).\n");
1640
+ console.log(" Tip: for a hard dependency, add `required = true` under [mcp_servers.niro]");
1641
+ console.log(" (Codex then fails startup if niro is unreachable; off by default).\n");
1621
1642
 
1622
1643
  // Clear per-tool overrides FIRST. Codex writes a [mcp_servers.niro.tools.<name>] table with
1623
1644
  // approval_mode = "approve" whenever the developer answers a prompt with "approve once" rather
@@ -1633,7 +1654,7 @@ function hardenCodexServerConfig() {
1633
1654
 
1634
1655
  const located = locateCodexNiroTable(cfgPath);
1635
1656
  if (!located) {
1636
- console.warn(" [mcp_servers.niro] not found in config.toml — skipping approval-mode hardening.\n");
1657
+ console.warn(" [mcp_servers.niro] not found in config.toml. Skipping approval-mode hardening.\n");
1637
1658
  return;
1638
1659
  }
1639
1660
  const { toml, bodyStart, bodyEnd, body } = located;
@@ -1641,7 +1662,7 @@ function hardenCodexServerConfig() {
1641
1662
  const existing = body.match(/^[ \t]*default_tools_approval_mode[ \t]*=[ \t]*"([^"]*)"/m);
1642
1663
 
1643
1664
  if (existing && existing[1] !== "auto") {
1644
- console.log(` Codex approval mode already set to "${existing[1]}" — leaving your choice alone.\n`);
1665
+ console.log(` Codex approval mode already set to "${existing[1]}", so your choice is left alone.\n`);
1645
1666
  return;
1646
1667
  }
1647
1668
 
@@ -1669,7 +1690,7 @@ function hardenCodexServerConfig() {
1669
1690
  message.forEach((line) => console.log(line));
1670
1691
  console.log("");
1671
1692
  } catch (err) {
1672
- console.warn(` Could not update the Codex approval mode: ${err.message}\n`);
1693
+ console.warn(` Could not update the Codex approval mode: ${err.message}\n`);
1673
1694
  }
1674
1695
  }
1675
1696
 
@@ -1718,18 +1739,18 @@ function addCodexMemory() {
1718
1739
 
1719
1740
  const { content, action } = upsertNiroMarkerSection(existing, section);
1720
1741
  if (action === "unchanged") {
1721
- console.log(" AGENTS.md entry already up to date — skipping.\n");
1742
+ console.log(" AGENTS.md entry already up to date. Skipping.\n");
1722
1743
  return;
1723
1744
  }
1724
1745
 
1725
1746
  try {
1726
1747
  fs.mkdirSync(path.dirname(agentsPath), { recursive: true });
1727
1748
  fs.writeFileSync(agentsPath, content);
1728
- console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ~/.codex/AGENTS.md`);
1729
- console.log(" → Codex will see Niro tool guidance across sessions.\n");
1749
+ console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ~/.codex/AGENTS.md`);
1750
+ console.log(" → Codex will see Niro tool guidance across sessions.\n");
1730
1751
  } catch (err) {
1731
- console.warn(` Could not write ~/.codex/AGENTS.md: ${err.message}`);
1732
- console.warn(" Add manually to ~/.codex/AGENTS.md:\n");
1752
+ console.warn(` Could not write ~/.codex/AGENTS.md: ${err.message}`);
1753
+ console.warn(" Add manually to ~/.codex/AGENTS.md:\n");
1733
1754
  console.warn(section + "\n");
1734
1755
  }
1735
1756
  }
@@ -1745,11 +1766,11 @@ function addAgentsSkill() {
1745
1766
  try {
1746
1767
  const result = writeManagedSkill(skillPath, NIRO_SKILL, "Shared niro-skill");
1747
1768
  if (result === "added" || result === "updated") {
1748
- console.log(" • Read by Codex and Windsurf (cross-agent skills location).");
1769
+ console.log(" • Read by Codex and Windsurf (cross-agent skills location).");
1749
1770
  }
1750
1771
  console.log("");
1751
1772
  } catch (err) {
1752
- console.warn(` Could not write ~/.agents/skills/niro-skill/SKILL.md: ${err.message}\n`);
1773
+ console.warn(` Could not write ~/.agents/skills/niro-skill/SKILL.md: ${err.message}\n`);
1753
1774
  }
1754
1775
  }
1755
1776
 
@@ -1763,11 +1784,11 @@ function addAgentsCliSkill() {
1763
1784
  try {
1764
1785
  const result = writeManagedSkill(skillPath, NIRO_CLI_SKILL, "Shared niro-cli skill");
1765
1786
  if (result === "added" || result === "updated") {
1766
- console.log(" • Read by Codex and Windsurf (cross-agent skills location).");
1787
+ console.log(" • Read by Codex and Windsurf (cross-agent skills location).");
1767
1788
  }
1768
1789
  console.log("");
1769
1790
  } catch (err) {
1770
- console.warn(` Could not write ~/.agents/skills/niro-cli/SKILL.md: ${err.message}\n`);
1791
+ console.warn(` Could not write ~/.agents/skills/niro-cli/SKILL.md: ${err.message}\n`);
1771
1792
  }
1772
1793
  }
1773
1794
 
@@ -1788,16 +1809,16 @@ function addWindsurfGlobalRule() {
1788
1809
  }
1789
1810
  const { content, action } = upsertNiroMarkerSection(existing, section);
1790
1811
  if (action === "unchanged") {
1791
- console.log(" Windsurf global rule already up to date — skipping.\n");
1812
+ console.log(" Windsurf global rule already up to date. Skipping.\n");
1792
1813
  return;
1793
1814
  }
1794
1815
  try {
1795
1816
  fs.mkdirSync(path.dirname(rulePath), { recursive: true });
1796
1817
  fs.writeFileSync(rulePath, content);
1797
- console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ~/.codeium/windsurf/memories/global_rules.md`);
1798
- console.log(" → Windsurf stays aware of Niro across sessions.\n");
1818
+ console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ~/.codeium/windsurf/memories/global_rules.md`);
1819
+ console.log(" → Windsurf stays aware of Niro across sessions.\n");
1799
1820
  } catch (err) {
1800
- console.warn(` Could not write Windsurf global_rules.md: ${err.message}\n`);
1821
+ console.warn(` Could not write Windsurf global_rules.md: ${err.message}\n`);
1801
1822
  }
1802
1823
  }
1803
1824
 
@@ -1816,15 +1837,15 @@ function appendMarkerSectionToFile(absPath, label, section = NIRO_GENERIC_MD_SEC
1816
1837
  }
1817
1838
  const { content, action } = upsertNiroMarkerSection(existing, section);
1818
1839
  if (action === "unchanged") {
1819
- console.log(` ${label} entry already up to date — skipping.\n`);
1840
+ console.log(` ${label} entry already up to date. Skipping.\n`);
1820
1841
  return;
1821
1842
  }
1822
1843
  try {
1823
1844
  fs.mkdirSync(path.dirname(absPath), { recursive: true });
1824
1845
  fs.writeFileSync(absPath, content);
1825
- console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ${absPath}\n`);
1846
+ console.log(` Niro reminder ${action === "refreshed" ? "refreshed in" : "added to"} ${absPath}\n`);
1826
1847
  } catch (err) {
1827
- console.warn(` Could not write ${absPath}: ${err.message}\n`);
1848
+ console.warn(` Could not write ${absPath}: ${err.message}\n`);
1828
1849
  }
1829
1850
  }
1830
1851
 
@@ -1876,12 +1897,12 @@ function addClineRule() {
1876
1897
  try {
1877
1898
  const result = writeManagedSkill(rulePath, NIRO_CLINE_RULE, "Cline rule");
1878
1899
  if (result === "added" || result === "updated") {
1879
- console.log(" • Global — applies across all your repos in Cline.");
1900
+ console.log(" • Global: applies across all your repos in Cline.");
1880
1901
  }
1881
1902
  console.log("");
1882
1903
  } catch (err) {
1883
- console.warn(` Could not write Cline rule: ${err.message}`);
1884
- console.warn(` If Cline uses ~/Cline/Rules instead of ~/Documents/Cline/Rules on your system, copy it there.\n`);
1904
+ console.warn(` Could not write Cline rule: ${err.message}`);
1905
+ console.warn(` If Cline uses ~/Cline/Rules instead of ~/Documents/Cline/Rules on your system, copy it there.\n`);
1885
1906
  }
1886
1907
  }
1887
1908
 
@@ -1915,11 +1936,11 @@ function addKiroSteering() {
1915
1936
  try {
1916
1937
  const result = writeManagedSkill(rulePath, NIRO_KIRO_STEERING, "Kiro steering file");
1917
1938
  if (result === "added" || result === "updated") {
1918
- console.log(" • Global — applies across all your repos in Kiro.");
1939
+ console.log(" • Global: applies across all your repos in Kiro.");
1919
1940
  }
1920
1941
  console.log("");
1921
1942
  } catch (err) {
1922
- console.warn(` Could not write Kiro steering file: ${err.message}\n`);
1943
+ console.warn(` Could not write Kiro steering file: ${err.message}\n`);
1923
1944
  }
1924
1945
  }
1925
1946
 
@@ -1928,7 +1949,7 @@ function addKiroSteering() {
1928
1949
  * `-s user` makes it a global (not project) registration, matching every other client here.
1929
1950
  */
1930
1951
  async function setupGeminiCli(apiKey, mcpServerUrl) {
1931
- console.log("\n Configuring Gemini CLI...\n");
1952
+ console.log("\n Configuring Gemini CLI...\n");
1932
1953
 
1933
1954
  const cmd = ["gemini", "mcp", "add", "-s", "user"];
1934
1955
  if (apiKey) cmd.push("-e", `NIRO_API_KEY=${apiKey}`);
@@ -1937,16 +1958,16 @@ async function setupGeminiCli(apiKey, mcpServerUrl) {
1937
1958
 
1938
1959
  try {
1939
1960
  execSync(cmd.join(" "), { stdio: "inherit" });
1940
- console.log(" MCP server registered in Gemini CLI (~/.gemini/settings.json).\n");
1961
+ console.log(" MCP server registered in Gemini CLI (~/.gemini/settings.json).\n");
1941
1962
  } catch (err) {
1942
- console.error(" Failed to register MCP server. Is the Gemini CLI installed?");
1943
- console.error(" Install: https://github.com/google-gemini/gemini-cli\n");
1944
- console.error(` Manual command:\n ${cmd.join(" ")}\n`);
1963
+ console.error(" Failed to register MCP server. Is the Gemini CLI installed?");
1964
+ console.error(" Install: https://github.com/google-gemini/gemini-cli\n");
1965
+ console.error(` Manual command:\n ${cmd.join(" ")}\n`);
1945
1966
  return false;
1946
1967
  }
1947
1968
 
1948
1969
  addGeminiMemory();
1949
- console.log(" Run `/mcp reload` inside Gemini CLI so the server + memory reload.\n");
1970
+ console.log(" Run `/mcp reload` inside Gemini CLI so the server + memory reload.\n");
1950
1971
  return true;
1951
1972
  }
1952
1973
 
@@ -1954,7 +1975,7 @@ async function setupGeminiCli(apiKey, mcpServerUrl) {
1954
1975
  * Registers the niro MCP entry with the GitHub Copilot CLI (writes ~/.copilot/mcp-config.json).
1955
1976
  */
1956
1977
  async function setupCopilotCli(apiKey, mcpServerUrl) {
1957
- console.log("\n Configuring GitHub Copilot CLI...\n");
1978
+ console.log("\n Configuring GitHub Copilot CLI...\n");
1958
1979
 
1959
1980
  const cmd = ["copilot", "mcp", "add", "niro"];
1960
1981
  if (apiKey) cmd.push("--env", `NIRO_API_KEY=${apiKey}`);
@@ -1963,15 +1984,15 @@ async function setupCopilotCli(apiKey, mcpServerUrl) {
1963
1984
 
1964
1985
  try {
1965
1986
  execSync(cmd.join(" "), { stdio: "inherit" });
1966
- console.log(" MCP server registered in Copilot CLI (~/.copilot/mcp-config.json).\n");
1987
+ console.log(" MCP server registered in Copilot CLI (~/.copilot/mcp-config.json).\n");
1967
1988
  } catch (err) {
1968
- console.error(" Failed to register MCP server. Is the Copilot CLI installed (npm i -g @github/copilot)?\n");
1969
- console.error(` Manual command:\n ${cmd.join(" ")}\n`);
1989
+ console.error(" Failed to register MCP server. Is the Copilot CLI installed (npm i -g @github/copilot)?\n");
1990
+ console.error(` Manual command:\n ${cmd.join(" ")}\n`);
1970
1991
  return false;
1971
1992
  }
1972
1993
 
1973
1994
  addCopilotCliMemory();
1974
- console.log(" Run `/mcp` inside the Copilot CLI to verify — servers are available immediately.\n");
1995
+ console.log(" Run `/mcp` inside the Copilot CLI to verify. Servers are available immediately.\n");
1975
1996
  return true;
1976
1997
  }
1977
1998
 
@@ -1982,7 +2003,7 @@ async function setupCopilotCli(apiKey, mcpServerUrl) {
1982
2003
  * ("niro mcp serve"), not user input, so static quoting is safe.
1983
2004
  */
1984
2005
  async function setupFactory(apiKey, mcpServerUrl) {
1985
- console.log("\n Configuring Factory (droid)...\n");
2006
+ console.log("\n Configuring Factory (droid)...\n");
1986
2007
 
1987
2008
  const cmd = ["droid", "mcp", "add", "niro", '"niro mcp serve"'];
1988
2009
  if (apiKey) cmd.push("--env", `NIRO_API_KEY=${apiKey}`);
@@ -1990,16 +2011,16 @@ async function setupFactory(apiKey, mcpServerUrl) {
1990
2011
 
1991
2012
  try {
1992
2013
  execSync(cmd.join(" "), { stdio: "inherit" });
1993
- console.log(" MCP server registered in Factory (~/.factory/mcp.json).\n");
2014
+ console.log(" MCP server registered in Factory (~/.factory/mcp.json).\n");
1994
2015
  } catch (err) {
1995
- console.error(" Failed to register MCP server. Is the droid CLI installed?");
1996
- console.error(" Install: https://docs.factory.ai\n");
1997
- console.error(` Manual command:\n ${cmd.join(" ")}\n`);
2016
+ console.error(" Failed to register MCP server. Is the droid CLI installed?");
2017
+ console.error(" Install: https://docs.factory.ai\n");
2018
+ console.error(` Manual command:\n ${cmd.join(" ")}\n`);
1998
2019
  return false;
1999
2020
  }
2000
2021
 
2001
2022
  addFactoryMemory();
2002
- console.log(" droid reloads automatically when ~/.factory/mcp.json changes.\n");
2023
+ console.log(" droid reloads automatically when ~/.factory/mcp.json changes.\n");
2003
2024
  return true;
2004
2025
  }
2005
2026
 
@@ -2040,9 +2061,9 @@ function gooseConfigPath() {
2040
2061
  }
2041
2062
 
2042
2063
  async function setupGoose(apiKey, mcpServerUrl) {
2043
- console.log("\n Configuring Goose...\n");
2064
+ console.log("\n Configuring Goose...\n");
2044
2065
  const configPath = gooseConfigPath();
2045
- console.log(` Config: ${configPath}\n`);
2066
+ console.log(` Config: ${configPath}\n`);
2046
2067
 
2047
2068
  const niroBlock = buildGooseNiroBlock(apiKey, mcpServerUrl);
2048
2069
  let existing = "";
@@ -2051,7 +2072,7 @@ async function setupGoose(apiKey, mcpServerUrl) {
2051
2072
  existedBefore = fs.existsSync(configPath);
2052
2073
  if (existedBefore) existing = fs.readFileSync(configPath, "utf-8");
2053
2074
  } catch (err) {
2054
- console.warn(` Could not read ${configPath}: ${err.message}\n`);
2075
+ console.warn(` Could not read ${configPath}: ${err.message}\n`);
2055
2076
  }
2056
2077
 
2057
2078
  // Matches exactly the " niro:" line install writes (2-space indent, nothing trailing) —
@@ -2059,7 +2080,7 @@ async function setupGoose(apiKey, mcpServerUrl) {
2059
2080
  // match an unrelated key named "niro" anywhere in the file and wrongly report "already
2060
2081
  // installed" without ever writing the real entry.
2061
2082
  if (/^ {2}niro:\s*$/m.test(existing)) {
2062
- console.log(" A `niro` extension entry already exists in config.yaml — skipping (edit it by hand if it needs updating).\n");
2083
+ console.log(" A `niro` extension entry already exists in config.yaml. Skipping (edit it by hand if it needs updating).\n");
2063
2084
  return true;
2064
2085
  }
2065
2086
 
@@ -2086,22 +2107,22 @@ async function setupGoose(apiKey, mcpServerUrl) {
2086
2107
  // else: hasOtherExtensionsForm — next stays null, falls through to the manual-paste path.
2087
2108
 
2088
2109
  if (next === null) {
2089
- console.log(" Your config.yaml has an `extensions:` key in a form this installer doesn't");
2090
- console.log(" auto-edit (to avoid risking a duplicate key). Add this block under it by hand:\n");
2110
+ console.log(" Your config.yaml has an `extensions:` key in a form this installer doesn't");
2111
+ console.log(" auto-edit (to avoid risking a duplicate key). Add this block under it by hand:\n");
2091
2112
  console.log(niroBlock);
2092
- console.log(`\n (full file: ${configPath})\n`);
2113
+ console.log(`\n (full file: ${configPath})\n`);
2093
2114
  return false;
2094
2115
  }
2095
2116
 
2096
2117
  try {
2097
2118
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
2098
2119
  fs.writeFileSync(configPath, next);
2099
- console.log(` MCP server added to Goose config: ${configPath}\n`);
2100
- console.log(" Restart Goose (or start a new session) so the extension loads.\n");
2120
+ console.log(` MCP server added to Goose config: ${configPath}\n`);
2121
+ console.log(" Restart Goose (or start a new session) so the extension loads.\n");
2101
2122
  return true;
2102
2123
  } catch (err) {
2103
- console.error(` Failed to write config: ${err.message}`);
2104
- console.error(" Add this manually under a top-level `extensions:` key in config.yaml:\n");
2124
+ console.error(` Failed to write config: ${err.message}`);
2125
+ console.error(" Add this manually under a top-level `extensions:` key in config.yaml:\n");
2105
2126
  console.log(`extensions:\n${niroBlock}`);
2106
2127
  return false;
2107
2128
  }
@@ -2119,13 +2140,13 @@ async function setupJsonConfig(clientId, apiKey, mcpServerUrl) {
2119
2140
  const client = CLIENTS[clientId];
2120
2141
  const configPathFn = client.configPath;
2121
2142
  if (!configPathFn) {
2122
- console.error(` No config path known for ${client.name}.`);
2143
+ console.error(` No config path known for ${client.name}.`);
2123
2144
  return false;
2124
2145
  }
2125
2146
 
2126
2147
  const configPath = configPathFn();
2127
- console.log(`\n Configuring ${client.name}...`);
2128
- console.log(` Config: ${configPath}\n`);
2148
+ console.log(`\n Configuring ${client.name}...`);
2149
+ console.log(` Config: ${configPath}\n`);
2129
2150
 
2130
2151
  // Read existing config or start fresh
2131
2152
  let config = {};
@@ -2160,37 +2181,37 @@ async function setupJsonConfig(clientId, apiKey, mcpServerUrl) {
2160
2181
  try {
2161
2182
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
2162
2183
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
2163
- console.log(` MCP server added to ${client.name} config.\n`);
2184
+ console.log(` MCP server added to ${client.name} config.\n`);
2164
2185
  if (clientId === "cursor") {
2165
2186
  ensureCursorPermissionsForNiro();
2166
2187
  ensureCursorRule();
2167
- console.log(" Quit and reopen Cursor (full restart) so MCP + permissions reload.\n");
2188
+ console.log(" Quit and reopen Cursor (full restart) so MCP + permissions reload.\n");
2168
2189
  }
2169
2190
  if (clientId === "windsurf") {
2170
2191
  addAgentsSkill();
2171
2192
  addAgentsCliSkill();
2172
2193
  addWindsurfGlobalRule();
2173
- console.log(" Restart Windsurf so MCP + rules reload.\n");
2194
+ console.log(" Restart Windsurf so MCP + rules reload.\n");
2174
2195
  }
2175
2196
  if (clientId === "opencode") {
2176
2197
  addOpenCodeMemory();
2177
- console.log(" Restart OpenCode so the MCP server reloads.\n");
2198
+ console.log(" Restart OpenCode so the MCP server reloads.\n");
2178
2199
  }
2179
2200
  if (clientId === "cline") {
2180
2201
  addClineRule();
2181
- console.log(" Reopen Cline's MCP Servers panel (or restart VS Code) to reload.\n");
2202
+ console.log(" Reopen Cline's MCP Servers panel (or restart VS Code) to reload.\n");
2182
2203
  }
2183
2204
  if (clientId === "kiro") {
2184
2205
  addKiroSteering();
2185
- console.log(" Kiro reconnects MCP servers automatically on config save.\n");
2206
+ console.log(" Kiro reconnects MCP servers automatically on config save.\n");
2186
2207
  }
2187
2208
  if (clientId === "antigravity") {
2188
- console.log(" Reload via Agent Settings → Customizations → Installed MCP Servers → Refresh.\n");
2209
+ console.log(" Reload via Agent Settings → Customizations → Installed MCP Servers → Refresh.\n");
2189
2210
  }
2190
2211
  return true;
2191
2212
  } catch (err) {
2192
- console.error(` Failed to write config: ${err.message}`);
2193
- console.error(` Manually add to ${configPath}:\n`);
2213
+ console.error(` Failed to write config: ${err.message}`);
2214
+ console.error(` Manually add to ${configPath}:\n`);
2194
2215
  console.log(JSON.stringify({ [topLevelKey]: { niro: config[topLevelKey].niro } }, null, 2));
2195
2216
  return false;
2196
2217
  }
@@ -2219,7 +2240,7 @@ async function main() {
2219
2240
  else if (args[i] === "--url" && args[i + 1]) urlArg = args[++i];
2220
2241
  }
2221
2242
  if (urlArg && !/^https?:\/\//i.test(urlArg)) {
2222
- console.error(`\n Invalid --url: "${urlArg}" — it must start with http:// or https://\n`);
2243
+ console.error(`\n Invalid --url: "${urlArg}". It must start with http:// or https://\n`);
2223
2244
  process.exit(1);
2224
2245
  }
2225
2246
 
@@ -2234,13 +2255,13 @@ async function main() {
2234
2255
  // ~/.niro/credentials.json). Only prompt for an API key when there is no cli-token.
2235
2256
  let apiKey = null;
2236
2257
  if (hasCliToken()) {
2237
- console.log("\n Using your `niro login` session — no API key needed.");
2238
- console.log(" The MCP server will authenticate with your CLI token from ~/.niro/credentials.json.\n");
2258
+ console.log("\n Using your `niro login` session, so no API key needed.");
2259
+ console.log(" The MCP server will authenticate with your CLI token from ~/.niro/credentials.json.\n");
2239
2260
  } else {
2240
- apiKey = await ask(rl, " Niro API key (from console.niroai.dev > Settings)", resolveApiKey());
2261
+ apiKey = await ask(rl, " Niro API key (from console.niroai.dev > Settings)", resolveApiKey());
2241
2262
  if (!apiKey) {
2242
- console.error("\n No credential found. Run `niro login` first (recommended), or paste an API key");
2243
- console.error(" from console.niroai.dev > Settings > Account.\n");
2263
+ console.error("\n No credential found. Run `niro login` first (recommended), or paste an API key");
2264
+ console.error(" from console.niroai.dev > Settings > Account.\n");
2244
2265
  rl.close();
2245
2266
  process.exit(1);
2246
2267
  }
@@ -2249,9 +2270,9 @@ async function main() {
2249
2270
  // The MCP server URL is resolved, not prompted — see resolveMcpServerUrl.
2250
2271
  // Printing the URL + its source keeps a wrong pick visible and correctable.
2251
2272
  const { url: mcpServerUrl, source: urlSource, explicit } = resolveMcpServerUrl(urlArg);
2252
- console.log(` MCP server URL: ${mcpServerUrl} (${urlSource})`);
2273
+ console.log(` MCP server URL: ${mcpServerUrl} (${urlSource})`);
2253
2274
  if (!urlArg) {
2254
- console.log(" Different backend? Re-run with: niro mcp install --url <url>\n");
2275
+ console.log(" Different backend? Re-run with: niro mcp install --url <url>\n");
2255
2276
  } else {
2256
2277
  console.log("");
2257
2278
  }
@@ -2263,20 +2284,29 @@ async function main() {
2263
2284
  try {
2264
2285
  config.save({ mcpServerUrl });
2265
2286
  } catch (err) {
2266
- console.warn(`\n Could not write ${config.FILE}: ${err.message}\n`);
2287
+ console.warn(`\n Could not write ${config.FILE}: ${err.message}\n`);
2267
2288
  }
2268
2289
  }
2269
2290
 
2291
+ // `--list` is a lookup, not an install: print every client and stop. rl is closed first,
2292
+ // or the process hangs on an open readline that nothing will ever answer.
2293
+ if (process.argv.includes("--list")) {
2294
+ console.log("");
2295
+ printClients({ all: true });
2296
+ rl.close();
2297
+ return;
2298
+ }
2299
+
2270
2300
  // Select client
2271
2301
  let selectedClient = clientArg;
2272
2302
  if (!selectedClient) {
2273
2303
  console.log("");
2274
2304
  printClients();
2275
- selectedClient = await ask(rl, " Which client to configure?", "claude-code");
2305
+ selectedClient = await ask(rl, " Which client to configure?", "claude-code");
2276
2306
  }
2277
2307
 
2278
2308
  if (!CLIENTS[selectedClient]) {
2279
- console.error(`\n Unknown client: ${selectedClient}`);
2309
+ console.error(`\n Unknown client: ${selectedClient}`);
2280
2310
  printClients();
2281
2311
  rl.close();
2282
2312
  process.exit(1);
@@ -2292,13 +2322,13 @@ async function main() {
2292
2322
  // only applies to some (e.g. a query-string URL with "&" is fine for Cursor/VS Code/etc.).
2293
2323
  if (CLI_SETUP[selectedClient]) {
2294
2324
  if (hasShellMetacharacters(mcpServerUrl)) {
2295
- console.error(`\n Refusing to use MCP server URL "${mcpServerUrl}" — it contains spaces or`);
2296
- console.error(" characters not allowed in a URL. Check --url / NIRO_MCP_SERVER_URL.\n");
2325
+ console.error(`\n Refusing to use MCP server URL "${mcpServerUrl}": it contains spaces or`);
2326
+ console.error(" characters not allowed in a URL. Check --url / NIRO_MCP_SERVER_URL.\n");
2297
2327
  process.exit(1);
2298
2328
  }
2299
2329
  if (apiKey && hasShellMetacharacters(apiKey)) {
2300
- console.error("\n Refusing to use that API key — it contains spaces or characters that aren't");
2301
- console.error(" valid in an API key. Copy it again from console.niroai.dev > Settings > Account.\n");
2330
+ console.error("\n Refusing to use that API key: it contains spaces or characters that aren't");
2331
+ console.error(" valid in an API key. Copy it again from console.niroai.dev > Settings > Account.\n");
2302
2332
  process.exit(1);
2303
2333
  }
2304
2334
  }
@@ -2314,12 +2344,15 @@ async function main() {
2314
2344
  }
2315
2345
 
2316
2346
  if (success) {
2317
- console.log(" Setup complete! Restart your AI assistant to start using Niro.\n");
2347
+ // One line about what all of that was for, instead of a paragraph under each item.
2348
+ console.log(" Those hooks and skills are what remind your assistant to use Niro, and let");
2349
+ console.log(" you drive the CLI from chat. `niro mcp uninstall` removes all of them.");
2350
+ console.log(" Setup complete! Restart your AI assistant to start using Niro.\n");
2318
2351
  if (selectedClient !== "claude-code") {
2319
- console.log(" Tip: drop a `.niro-project` file at the repo root (single line");
2320
- console.log(" with the project_id) to pin that tree to a Niro project.");
2321
- console.log(" Without it, Niro runs in multi-project mode and discovers");
2322
- console.log(" projects via the list_projects tool.\n");
2352
+ console.log(" Tip: drop a `.niro-project` file at the repo root (single line");
2353
+ console.log(" with the project_id) to pin that tree to a Niro project.");
2354
+ console.log(" Without it, Niro runs in multi-project mode and discovers");
2355
+ console.log(" projects via the list_projects tool.\n");
2323
2356
  }
2324
2357
  }
2325
2358
  }
@@ -2335,6 +2368,11 @@ if (require.main === module) {
2335
2368
  }
2336
2369
 
2337
2370
  module.exports = {
2371
+ // Exported so `niro ab` resolves the ai-assistant base URL through the SAME precedence the
2372
+ // MCP installer uses (--url > NIRO_MCP_SERVER_URL > saved config > cloud default). A second
2373
+ // resolver would drift the first time that order changed, and the two would then disagree
2374
+ // about which backend a machine is pointed at.
2375
+ resolveMcpServerUrl,
2338
2376
  writeManagedSkill,
2339
2377
  addClaudeCodeSkill,
2340
2378
  addClaudeCodeCliSkill,