@niroai/niro 0.16.0 → 0.18.0

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.
Files changed (177) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +123 -302
  3. package/bin/niro.js +12 -1
  4. package/docs/agent-antigravity.md +37 -0
  5. package/docs/agent-claude-code.md +48 -0
  6. package/docs/agent-claude-desktop.md +47 -0
  7. package/docs/agent-cline.md +42 -0
  8. package/docs/agent-codex.md +45 -0
  9. package/docs/agent-copilot-cli.md +42 -0
  10. package/docs/agent-cursor.md +44 -0
  11. package/docs/agent-factory.md +42 -0
  12. package/docs/agent-gemini-cli.md +42 -0
  13. package/docs/agent-goose.md +42 -0
  14. package/docs/agent-kiro.md +37 -0
  15. package/docs/agent-opencode.md +37 -0
  16. package/docs/agent-vscode.md +45 -0
  17. package/docs/agent-windsurf.md +38 -0
  18. package/docs/answer-shapes.md +69 -0
  19. package/docs/builds-and-updates.md +68 -0
  20. package/docs/cloud-README.md +372 -0
  21. package/docs/configuring-a-folder.md +51 -0
  22. package/docs/connect-ci.md +24 -0
  23. package/docs/connect-overview.md +20 -0
  24. package/docs/connect-review-bots.md +17 -0
  25. package/docs/connect-team-server.md +12 -0
  26. package/docs/connect-terminal.md +49 -0
  27. package/docs/connect-web-agents.md +9 -0
  28. package/docs/connect-your-ai-assistant.md +59 -175
  29. package/docs/coverage-audit-and-consent.md +51 -0
  30. package/docs/evaluate-what-niro-did.md +271 -0
  31. package/docs/faq-assistants-and-mcp.md +122 -0
  32. package/docs/faq-feedback-and-receipts.md +33 -0
  33. package/docs/faq-indexing.md +127 -0
  34. package/docs/faq-install-and-setup.md +103 -0
  35. package/docs/faq-licence-and-plans.md +86 -0
  36. package/docs/faq-login-and-account.md +69 -0
  37. package/docs/faq-offline.md +34 -0
  38. package/docs/faq-performance.md +33 -0
  39. package/docs/faq-privacy-and-security.md +113 -0
  40. package/docs/faq-uninstall.md +47 -0
  41. package/docs/faq-updates.md +59 -0
  42. package/docs/faq-worktrees-and-branches.md +59 -0
  43. package/docs/faq-wrong-or-missing-answers.md +275 -0
  44. package/docs/hosted-only.md +33 -0
  45. package/docs/how-the-graph-works.md +47 -0
  46. package/docs/index.md +157 -0
  47. package/docs/licence-and-plans.md +72 -0
  48. package/docs/lifecycle-debug.md +48 -0
  49. package/docs/lifecycle-onboarding.md +32 -0
  50. package/docs/lifecycle-overview.md +77 -0
  51. package/docs/lifecycle-plan-and-design.md +44 -0
  52. package/docs/lifecycle-review.md +30 -0
  53. package/docs/lifecycle-security-and-privacy.md +35 -0
  54. package/docs/lifecycle-test.md +28 -0
  55. package/docs/lifecycle-write-code.md +35 -0
  56. package/docs/llms-full.txt +8015 -0
  57. package/docs/llms.txt +126 -0
  58. package/docs/playbook-add-a-feature.md +108 -0
  59. package/docs/playbook-change-safely.md +93 -0
  60. package/docs/playbook-data-and-events.md +107 -0
  61. package/docs/playbook-debug-after-a-change.md +96 -0
  62. package/docs/playbook-debug-config.md +95 -0
  63. package/docs/playbook-debug-exception.md +124 -0
  64. package/docs/playbook-debug-failing-service-call.md +119 -0
  65. package/docs/playbook-debug-slow-path.md +80 -0
  66. package/docs/playbook-debug-wrong-value.md +137 -0
  67. package/docs/playbook-onboard.md +95 -0
  68. package/docs/playbook-review-a-change.md +72 -0
  69. package/docs/playbook-test-a-change.md +73 -0
  70. package/docs/reference-cli.md +113 -0
  71. package/docs/reference-config-files.md +42 -0
  72. package/docs/reference-engine-commands.md +56 -0
  73. package/docs/reference-environment-variables.md +89 -0
  74. package/docs/reference-exit-codes.md +67 -0
  75. package/docs/reference-limits.md +75 -0
  76. package/docs/reference-mcp-tools.md +326 -0
  77. package/docs/reference-messages.md +180 -0
  78. package/docs/secrets-and-masking.md +48 -0
  79. package/docs/supported-c.md +130 -0
  80. package/docs/supported-cpp.md +157 -0
  81. package/docs/supported-csharp.md +211 -0
  82. package/docs/supported-go.md +258 -0
  83. package/docs/supported-java.md +226 -0
  84. package/docs/supported-javascript.md +223 -0
  85. package/docs/supported-kotlin.md +124 -0
  86. package/docs/supported-overview.md +89 -0
  87. package/docs/supported-php.md +157 -0
  88. package/docs/supported-python.md +224 -0
  89. package/docs/supported-ruby.md +162 -0
  90. package/docs/supported-rust.md +150 -0
  91. package/docs/supported-swift.md +133 -0
  92. package/docs/supported-typescript.md +220 -0
  93. package/docs/tokens-speed-and-accuracy.md +99 -0
  94. package/docs/troubleshooting.md +42 -0
  95. package/docs/uninstall.md +74 -0
  96. package/docs/what-leaves-your-machine.md +71 -0
  97. package/docs/where-things-are-stored.md +59 -0
  98. package/docs/worktrees-and-branches.md +55 -0
  99. package/package.json +5 -3
  100. package/src/commands/add-repo.js +5 -0
  101. package/src/commands/auditSend.js +67 -0
  102. package/src/commands/build.js +6 -0
  103. package/src/commands/docsSend.js +73 -0
  104. package/src/commands/engine.js +120 -0
  105. package/src/commands/engineRoutes.js +105 -0
  106. package/src/commands/guide.js +254 -0
  107. package/src/commands/init.js +16 -7
  108. package/src/commands/initLocal.js +96 -0
  109. package/src/commands/journalSend.js +47 -0
  110. package/src/commands/login.js +89 -12
  111. package/src/commands/mcp.js +22 -6
  112. package/src/commands/plan.js +99 -0
  113. package/src/commands/repoPush.js +33 -0
  114. package/src/commands/requestIntegration.js +38 -0
  115. package/src/commands/share.js +56 -0
  116. package/src/commands/start.js +451 -371
  117. package/src/commands/status.js +57 -0
  118. package/src/commands/takeover.js +6 -0
  119. package/src/commands/uninstall.js +180 -0
  120. package/src/commands/upgrade.js +38 -8
  121. package/src/commands/watch.js +420 -6
  122. package/src/index.js +174 -3
  123. package/src/lib/agentApi.js +32 -1
  124. package/src/lib/api.js +20 -6
  125. package/src/lib/auditUpload.js +293 -0
  126. package/src/lib/branchDrift.js +149 -0
  127. package/src/lib/choose.js +116 -0
  128. package/src/lib/cloudGuard.js +74 -0
  129. package/src/lib/credentials.js +2 -2
  130. package/src/lib/docsMarked.js +55 -0
  131. package/src/lib/docsState.js +213 -0
  132. package/src/lib/docsUpload.js +481 -0
  133. package/src/lib/engine.js +619 -0
  134. package/src/lib/folderSync.js +2 -1
  135. package/src/lib/graphCheck.js +40 -0
  136. package/src/lib/guide.js +293 -0
  137. package/src/lib/guideSearch.js +252 -0
  138. package/src/lib/integrationRequest.js +89 -0
  139. package/src/lib/journalUpload.js +147 -0
  140. package/src/lib/languages.js +129 -0
  141. package/src/lib/lease.js +375 -0
  142. package/src/lib/localBuild.js +285 -0
  143. package/src/lib/localEngine.js +210 -0
  144. package/src/lib/localOnboard.js +124 -0
  145. package/src/lib/localWorktrees.js +243 -0
  146. package/src/lib/machineCheck.js +58 -0
  147. package/src/lib/mcpClients.js +10 -1
  148. package/src/lib/migrationNotice.js +118 -0
  149. package/src/lib/modelFetch.js +153 -0
  150. package/src/lib/nestedOutput.js +23 -9
  151. package/src/lib/onboardingLog.js +34 -0
  152. package/src/lib/planUsage.js +165 -0
  153. package/src/lib/progressBar.js +46 -0
  154. package/src/lib/prompt.js +1 -1
  155. package/src/lib/redact.js +155 -0
  156. package/src/lib/release.js +33 -0
  157. package/src/lib/repoPush.js +495 -0
  158. package/src/lib/secret.js +1 -1
  159. package/src/lib/service.js +372 -63
  160. package/src/lib/shareCode.js +367 -0
  161. package/src/lib/shareExclusions.js +114 -0
  162. package/src/lib/shareFiles.js +129 -0
  163. package/src/lib/spinner.js +9 -5
  164. package/src/lib/watchDaemon.js +33 -10
  165. package/src/lib/watchState.js +12 -1
  166. package/src/lib/windowsPaths.js +67 -0
  167. package/src/mcp/lineCheck.js +382 -0
  168. package/src/mcp/lineName.js +92 -0
  169. package/src/mcp/localBackend.js +751 -0
  170. package/src/mcp/localMode.js +383 -0
  171. package/src/mcp/localSkill.js +189 -0
  172. package/src/mcp/server.js +610 -79
  173. package/src/mcp/setup.js +519 -131
  174. package/src/mcp/shellSearchHook.js +509 -0
  175. package/src/mcp/teardown.js +173 -91
  176. package/src/mcp/usingNiro.js +39 -0
  177. package/docs/working-on-branches.md +0 -162
@@ -11,7 +11,6 @@
11
11
  * - the niro section in markdown config (CLAUDE.md / AGENTS.md / Windsurf rules)
12
12
  * - the generated skills (~/.claude/skills, the shared ~/.agents/skills)
13
13
  * - Cursor's "niro:*" allowlist entry and project rule
14
- * - (with --all) the mcpServerUrl fallback in ~/.niro/config.json
15
14
  *
16
15
  * It deliberately LEAVES ~/.niro/credentials.json (your `niro login` session) alone —
17
16
  * use `niro logout` for that — and never deletes a user's own CLAUDE.md/AGENTS.md, only
@@ -33,11 +32,14 @@ const path = require("path");
33
32
  const readline = require("readline");
34
33
  const { execSync } = require("child_process");
35
34
  const { formatPrompt } = require("../lib/prompt");
35
+ const { choose } = require("../lib/choose");
36
+ const color = require("../lib/color");
36
37
  const {
37
38
  CLIENTS,
38
39
  NIRO_MD_MARKER,
39
- NIRO_MD_SECTIONS,
40
+ NIRO_ALL_MD_SECTIONS,
40
41
  isNiroStopHook,
42
+ isNiroLineCheckHook,
41
43
  isNiroPostCompactHook,
42
44
  isNiroUserPromptHook,
43
45
  isNiroPreToolUseHook,
@@ -45,6 +47,7 @@ const {
45
47
  isNiroImpactHook,
46
48
  userHome,
47
49
  } = require("./setup");
50
+ const { isNiroShellSearchHook } = require("./shellSearchHook");
48
51
 
49
52
  // ─── pure helpers (unit-tested — no fs, no $HOME) ────────────────────────────
50
53
 
@@ -55,14 +58,22 @@ const {
55
58
  * Primary path: delete the EXACT section text install wrote. Because we match the whole known
56
59
  * block (not "marker → next heading / EOF"), a user's own notes added BELOW the block survive,
57
60
  * the file is restored byte-for-byte, duplicate blocks are all removed, and a differently-worded
58
- * example of the marker elsewhere is untouched. Both LF and CRLF copies are tried.
61
+ * example of the marker elsewhere is untouched. Both LF and CRLF copies are tried. The text cut out
62
+ * starts and ends with a line break, so when the user's text touches it on both sides one line
63
+ * break is put back; without it the line above and the line below were joined into one (check of
64
+ * feat/niro-guide, 2026-11-10: 8 of 91 user lines on upgrade, 10 on uninstall).
59
65
  *
60
66
  * Fallback (the section text drifted — older install version or a hand-edit): a BOUNDED removal
61
- * that strips the marker line and the "## Niro MCP" block it heads but stops at the first blank
62
- * line / next heading / EOF, and refuses to touch a marker sitting inside a ``` code fence — so
63
- * trailing user content is never swallowed. Returns { content, removed }.
67
+ * that strips the marker line and, in the lines under it up to a blank line or EOF, only lines some
68
+ * known section holds as a whole line. It refuses to touch a marker sitting inside a ``` code fence.
69
+ * Returns { content, removed }.
70
+ *
71
+ * The choice, where niro cannot tell where an unknown section ends: keep the line. A line no known
72
+ * section holds is never deleted, so a hand-edited Niro line, or one an older release worded
73
+ * differently, may stay behind; that is safer than deleting the user's own line glued below
74
+ * (Niraj, 2026-11-10: "sure"). Before this, the whole block went, the user's glued line with it.
64
75
  */
65
- function removeMarkerSection(content, marker, knownSections = NIRO_MD_SECTIONS) {
76
+ function removeMarkerSection(content, marker, knownSections = NIRO_ALL_MD_SECTIONS) {
66
77
  if (typeof content !== "string" || !content.includes(marker)) {
67
78
  return { content, removed: false };
68
79
  }
@@ -72,9 +83,11 @@ function removeMarkerSection(content, marker, knownSections = NIRO_MD_SECTIONS)
72
83
 
73
84
  // Primary: exact removal of every known section (LF, then a CRLF copy).
74
85
  for (const section of knownSections || []) {
75
- for (const variant of [section, section.replace(/\n/g, "\r\n")]) {
76
- while (next.includes(variant)) {
77
- next = next.replace(variant, "");
86
+ for (const [variant, eol] of [[section, "\n"], [section.replace(/\n/g, "\r\n"), "\r\n"]]) {
87
+ let at;
88
+ while ((at = next.indexOf(variant)) !== -1) {
89
+ const after = at + variant.length;
90
+ next = next.slice(0, at) + (touchesTextOnBothSides(next, at, after) ? eol : "") + next.slice(after);
78
91
  removed = true;
79
92
  }
80
93
  }
@@ -84,7 +97,7 @@ function removeMarkerSection(content, marker, knownSections = NIRO_MD_SECTIONS)
84
97
  // handled; stop when no marker remains or a pass makes no progress (e.g. an in-fence marker
85
98
  // we deliberately leave alone), which also prevents an infinite loop.
86
99
  while (next.includes(marker)) {
87
- const r = removeMarkerSectionBounded(next, marker);
100
+ const r = removeMarkerSectionBounded(next, marker, knownSections);
88
101
  if (!r.removed || r.content === next) break;
89
102
  next = r.content;
90
103
  removed = true;
@@ -94,13 +107,27 @@ function removeMarkerSection(content, marker, knownSections = NIRO_MD_SECTIONS)
94
107
  }
95
108
 
96
109
  /**
97
- * Bounded fallback for removeMarkerSection: remove the marker line and the "## Niro MCP" block
98
- * it heads, stopping at the first blank line, the next heading, or EOF. A marker inside a code
99
- * fence is treated as documentation and left untouched. Operates line-wise so CRLF survives.
110
+ * True when the text cut out of `content` between `at` and `after` had the user's text right against it on both
111
+ * sides: something other than a line break just before, and something other than a line break just after. The cut
112
+ * text starts and ends with a line break, so the two lines would be joined unless one is put back.
100
113
  */
101
- function removeMarkerSectionBounded(content, marker) {
102
- const eol = content.includes("\r\n") ? "\r\n" : "\n";
103
- const lines = content.split(/\r?\n/);
114
+ function touchesTextOnBothSides(content, at, after) {
115
+ if (at === 0 || after >= content.length) return false;
116
+ return content[at - 1] !== "\n" && content[after] !== "\n" && content[after] !== "\r";
117
+ }
118
+
119
+ /**
120
+ * Bounded fallback for removeMarkerSection: remove the marker line and, in the block under it (up to a blank
121
+ * line or EOF), every line some known section holds as a whole line (the "## Niro MCP" heading and body lines
122
+ * a release wrote). A line no known section holds is kept, so a user's line glued below an unknown section
123
+ * survives. A marker inside a code fence is treated as documentation
124
+ * and left untouched. A marker sharing its line with other text loses only the marker. Each kept line keeps
125
+ * its own line ending.
126
+ */
127
+ function removeMarkerSectionBounded(content, marker, knownSections = NIRO_ALL_MD_SECTIONS) {
128
+ // Each line keeps its own line ending, so LF, CRLF and a last line with no line break all come back as they were.
129
+ const lines = content.split(/(?<=\n)/);
130
+ const text = (line) => line.replace(/\r?\n$/, "");
104
131
  const markerLine = lines.findIndex((l) => l.includes(marker));
105
132
  if (markerLine === -1) return { content, removed: false };
106
133
 
@@ -111,23 +138,33 @@ function removeMarkerSectionBounded(content, marker) {
111
138
  }
112
139
  if (fences % 2 === 1) return { content, removed: false };
113
140
 
114
- let end = markerLine + 1; // exclusive index of the first line to KEEP
115
- if (end < lines.length && lines[end].startsWith("## ")) {
116
- end++; // the "## Niro MCP" heading
117
- while (end < lines.length && lines[end].trim() !== "" && !lines[end].startsWith("#")) {
118
- end++; // contiguous niro body lines — stop at a blank line or the next heading
119
- }
120
- }
121
-
122
- let begin = markerLine;
123
- if (begin > 0 && lines[begin - 1].trim() === "") begin--; // drop install's separator blank line
141
+ // The marker shares its line with the user's text: that line keeps the user's text and loses only the marker.
142
+ const rest = lines[markerLine].split(marker).join("");
143
+ const markerAlone = text(rest).trim() === "";
144
+ if (!markerAlone) lines[markerLine] = rest;
124
145
 
125
- lines.splice(begin, end - begin);
126
- return { content: lines.join(eol), removed: true };
146
+ const knownLines = new Set();
147
+ for (const section of knownSections || []) {
148
+ for (const line of section.split(/\r?\n/)) if (line.trim() !== "") knownLines.add(line.trimEnd());
149
+ }
150
+ // The block is the lines right under the marker, up to a blank line or EOF. Of those, only a line some release
151
+ // wrote word for word goes (the heading, a body line). A line no release wrote stays where it is: the user's
152
+ // own line glued below, and also a Niro line the user edited or an older release worded differently, because
153
+ // niro cannot tell those apart from the user's. Known lines after it still go, so an edited line leaves only
154
+ // itself behind, not the rest of the old body.
155
+ const drop = new Set();
156
+ if (markerAlone) {
157
+ drop.add(markerLine);
158
+ if (markerLine > 0 && text(lines[markerLine - 1]).trim() === "") drop.add(markerLine - 1); // install's separator blank line
159
+ }
160
+ for (let i = markerLine + 1; i < lines.length && text(lines[i]).trim() !== ""; i++) {
161
+ if (knownLines.has(text(lines[i]).trimEnd())) drop.add(i);
162
+ }
163
+ return { content: lines.filter((_, i) => !drop.has(i)).join(""), removed: true };
127
164
  }
128
165
 
129
166
  /**
130
- * Strip niro's PostCompact, UserPromptSubmit, and PreToolUse (temp-project + explore-first + impact-gate) hook
167
+ * Strip niro's PostCompact, UserPromptSubmit, and PreToolUse (temp-project + explore-first + shell-search + impact-gate) hook
131
168
  * entries from a parsed Claude Code settings object (mutates and returns it). Uses the same matchers
132
169
  * install uses. Empty arrays / an empty `hooks` object are pruned so the file stays tidy. Other hooks
133
170
  * are untouched. Returns { settings, removed } where `removed` is the count deleted.
@@ -154,14 +191,14 @@ function filterNiroHooks(settings) {
154
191
  if (Array.isArray(hooks.PreToolUse)) {
155
192
  const before = hooks.PreToolUse.length;
156
193
  hooks.PreToolUse = hooks.PreToolUse.filter(
157
- (h) => !isNiroPreToolUseHook(h) && !isNiroExploreHook(h) && !isNiroImpactHook(h),
194
+ (h) => !isNiroPreToolUseHook(h) && !isNiroExploreHook(h) && !isNiroImpactHook(h) && !isNiroShellSearchHook(h),
158
195
  );
159
196
  removed += before - hooks.PreToolUse.length;
160
197
  if (hooks.PreToolUse.length === 0) delete hooks.PreToolUse;
161
198
  }
162
199
  if (Array.isArray(hooks.Stop)) {
163
200
  const before = hooks.Stop.length;
164
- hooks.Stop = hooks.Stop.filter((h) => !isNiroStopHook(h));
201
+ hooks.Stop = hooks.Stop.filter((h) => !isNiroStopHook(h) && !isNiroLineCheckHook(h));
165
202
  removed += before - hooks.Stop.length;
166
203
  if (hooks.Stop.length === 0) delete hooks.Stop;
167
204
  }
@@ -377,6 +414,7 @@ function forceRemoveAgentsSkills() {
377
414
  const home = userHome();
378
415
  deleteFileIfPresent(path.join(home, ".agents", "skills", "niro-skill", "SKILL.md"), "shared niro skill");
379
416
  deleteFileIfPresent(path.join(home, ".agents", "skills", "niro-cli", "SKILL.md"), "shared niro-cli skill");
417
+ deleteFileIfPresent(path.join(home, ".agents", "skills", "niro-local", "SKILL.md"), "shared niro-local skill");
380
418
  }
381
419
 
382
420
  // ─── per-client uninstallers (mirror setup.js's per-client installers) ────────
@@ -399,6 +437,15 @@ function uninstallClaudeCode() {
399
437
  stripMarkerFromFile(path.join(home, ".claude", "CLAUDE.md"), "Claude Code memory");
400
438
  deleteFileIfPresent(path.join(home, ".claude", "skills", "niro-skill", "SKILL.md"), "niro skill");
401
439
  deleteFileIfPresent(path.join(home, ".claude", "skills", "niro-cli", "SKILL.md"), "niro-cli skill");
440
+ deleteFileIfPresent(path.join(home, ".claude", "skills", "niro-local", "SKILL.md"), "niro-local skill");
441
+ // The saved mode (`niro mcp install --local`) goes too, so a later plain install starts from the
442
+ // cloud default rather than silently from local mode (verifier, 2026-11-02).
443
+ try {
444
+ const config = require("../lib/config");
445
+ if (config.load().mcpMode !== undefined) config.save({ mcpMode: undefined });
446
+ } catch {
447
+ /* best effort, as above */
448
+ }
402
449
  }
403
450
 
404
451
  function uninstallCodex(opts = {}) {
@@ -545,38 +592,8 @@ const UNINSTALLERS = {
545
592
  antigravity: uninstallAntigravity,
546
593
  };
547
594
 
548
- /**
549
- * Drop the mcpServerUrl fallback install saved to ~/.niro/config.json. Only invoked for a
550
- * full (--all) uninstall — it's a global, not per-client, value. apiUrl and any other keys
551
- * are preserved; credentials.json is never touched here.
552
- */
553
- function removeGlobalMcpServerUrl() {
554
- let cfg;
555
- try {
556
- cfg = require("../lib/config");
557
- } catch {
558
- return;
559
- }
560
- try {
561
- const current = cfg.load();
562
- if (!Object.prototype.hasOwnProperty.call(current, "mcpServerUrl")) return;
563
- delete current.mcpServerUrl;
564
- fs.writeFileSync(cfg.FILE, JSON.stringify(current, null, 2) + "\n");
565
- console.log(` Removed mcpServerUrl from ${cfg.FILE}`);
566
- } catch (e) {
567
- console.warn(` Could not update ~/.niro/config.json: ${e.message}`);
568
- }
569
- }
570
-
571
595
  // ─── interactive entry ───────────────────────────────────────────────────────
572
596
 
573
- function printHeader() {
574
- console.log("");
575
- console.log(" Niro MCP Server Uninstall");
576
- console.log(" -------------------------");
577
- console.log("");
578
- }
579
-
580
597
  function printClients() {
581
598
  console.log(" Clients:");
582
599
  Object.keys(UNINSTALLERS).forEach((id) => {
@@ -604,18 +621,60 @@ function ask(rl, question, defaultValue) {
604
621
  });
605
622
  }
606
623
 
624
+ /**
625
+ * Remove Niro from `targets`, one line per assistant. Each uninstaller's own step-by-step lines
626
+ * are kept back and shown only when that assistant fails, so a normal run reads as a checklist.
627
+ */
607
628
  function runUninstall(targets, isFull) {
608
- for (const id of targets) UNINSTALLERS[id]({ full: isFull });
629
+ let failed = 0;
630
+ for (const id of targets) {
631
+ const said = [];
632
+ const [log, warn] = [console.log, console.warn];
633
+ console.log = (...a) => said.push(a.join(" "));
634
+ console.warn = (...a) => said.push(a.join(" "));
635
+ try {
636
+ UNINSTALLERS[id]({ full: isFull });
637
+ console.log = log;
638
+ console.warn = warn;
639
+ console.log(`│ ${color.green("✓")} ${nameOf(id)}`);
640
+ } catch (err) {
641
+ console.log = log;
642
+ console.warn = warn;
643
+ failed += 1;
644
+ console.log(`│ ${color.yellow("⚠")} ${nameOf(id)}: ${err.message}`);
645
+ for (const line of said) if (line.trim()) console.log(`│ ${line.trim()}`);
646
+ }
647
+ }
609
648
  if (isFull) {
610
- // Every editor is gone — remove the shared cross-agent skills and the global URL.
611
- forceRemoveAgentsSkills();
612
- removeGlobalMcpServerUrl();
649
+ // Every editor is gone: remove the shared cross-agent skills. The saved mcpServerUrl stays: it
650
+ // is also where usage is sent, and removing it sent a dev or staging machine's usage to
651
+ // production (`niro uninstall` deletes ~/.niro, and it, anyway).
652
+ const [log] = [console.log];
653
+ console.log = () => {};
654
+ try {
655
+ forceRemoveAgentsSkills();
656
+ } finally {
657
+ console.log = log;
658
+ }
613
659
  }
660
+ console.log("│");
661
+ console.log(`└ ${failed ? "Done, with the problems above." : "Done."} Restart ${targets.length === 1 ? nameOf(targets[0]) : "those assistants"} for it to take effect.`);
662
+ console.log("");
663
+ console.log(` Your sign-in and your graphs are kept. To remove Niro completely: ${color.cyan("niro uninstall")}`);
664
+ console.log("");
665
+ if (failed) process.exitCode = 1;
666
+ }
667
+
668
+ const nameOf = (id) => (CLIENTS[id] ? CLIENTS[id].name : id);
614
669
 
615
- console.log("\n Uninstall complete. Restart your AI assistant for the change to take effect.\n");
616
- console.log(" Your `niro login` session is kept. Run `niro logout` to clear it.");
617
- console.log(" The `niro` command itself is still installed. To remove it too:");
618
- console.log(" npm rm -g niro-mcp-and-cli\n");
670
+ /** The assistants Niro is installed in on this machine, by id, each once. */
671
+ function installedIds() {
672
+ try {
673
+ const { configured } = require("../lib/mcpClients").configuredClients();
674
+ return [...new Set(configured.map((c) => c.id))].filter((id) => UNINSTALLERS[id]);
675
+ } catch {
676
+ return [];
677
+ }
619
678
  }
620
679
 
621
680
  async function main() {
@@ -629,52 +688,75 @@ async function main() {
629
688
  else if (args[i] === "-y" || args[i] === "--yes") assumeYes = true;
630
689
  }
631
690
 
632
- printHeader();
633
-
634
691
  const allIds = Object.keys(UNINSTALLERS);
635
692
  let targets;
693
+ let picked = false;
694
+
695
+ // Nothing to do is one plain line, not a box around one sentence.
696
+ const interactive = !all && clientArg !== "all" && !clientArg && process.stdin.isTTY;
697
+ const installed = interactive ? installedIds() : null;
698
+ if (interactive && !installed.length) {
699
+ console.log(`\nNiro is not set up in any AI assistant on this machine, so there is nothing to remove. To set it up: ${color.cyan("niro mcp install")}\n`);
700
+ return;
701
+ }
702
+
703
+ console.log("");
704
+ console.log("┌ Remove Niro from your AI assistants");
705
+ console.log("│");
636
706
 
637
707
  if (all || clientArg === "all") {
638
708
  targets = allIds;
639
709
  } else if (clientArg) {
640
- if (!UNINSTALLERS[clientArg]) {
641
- console.error(` Unknown client: ${clientArg}\n`);
710
+ const ids = clientArg.split(",").map((x) => x.trim()).filter(Boolean);
711
+ const unknown = ids.filter((id) => !UNINSTALLERS[id]);
712
+ if (unknown.length) {
713
+ console.error(`│ Unknown assistant: ${unknown.join(", ")}`);
642
714
  printClients();
643
715
  process.exit(1);
644
716
  }
645
- targets = [clientArg];
717
+ targets = ids;
646
718
  } else if (!process.stdin.isTTY) {
647
719
  // Non-interactive with no target: refuse rather than guess.
648
- console.error(" No client specified. Pass --client <id> or --all.\n");
720
+ console.error("│ No assistant named. Pass --client <id> or --all.");
649
721
  printClients();
650
722
  process.exit(1);
651
723
  } else {
652
- printClients();
653
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
654
- const choice = await ask(rl, " Remove Niro from which client?", "claude-code");
655
- rl.close();
656
- if (choice === "all") targets = allIds;
657
- else if (UNINSTALLERS[choice]) targets = [choice];
658
- else {
659
- console.error(`\n Unknown client: ${choice}\n`);
660
- process.exit(1);
724
+ console.log("◆ Which assistants should stop using Niro?");
725
+ targets = await choose({
726
+ prefix: "│ ",
727
+ items: installed.map((id) => ({ value: id, label: nameOf(id) })),
728
+ multi: true,
729
+ // Nothing ticked to start: pre-ticked, "space on each" unticked them all and Enter then
730
+ // took only the one under the cursor (Divya, 2026-11-05).
731
+ initial: [],
732
+ hint: "↑/↓ move · space tick · a all · enter remove",
733
+ });
734
+ picked = true;
735
+ if (!targets.length) {
736
+ console.log("└ Nothing picked. Nothing was changed.");
737
+ console.log("");
738
+ return;
661
739
  }
662
740
  }
663
741
 
664
- const isFull = targets.length === allIds.length;
742
+ // Everything that was set up is going, so the shared files go too.
743
+ const remaining = installedIds().filter((id) => !targets.includes(id));
744
+ const isFull = targets.length === allIds.length || remaining.length === 0;
665
745
 
666
- if (!assumeYes && process.stdin.isTTY) {
667
- console.log(`\n This removes Niro MCP config from: ${targets.join(", ")}`);
668
- console.log(" (Your `niro login` session in ~/.niro/credentials.json is left intact.)\n");
746
+ // Picking in the list above is the confirmation; a command line naming assistants asks once.
747
+ if (!picked && !assumeYes && process.stdin.isTTY) {
748
+ console.log(`│ This removes Niro from: ${targets.map(nameOf).join(", ")}`);
669
749
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
670
- const ans = await ask(rl, " Proceed?", "yes");
750
+ const ans = await ask(rl, "│ Go ahead?", "yes");
671
751
  rl.close();
672
752
  if (!/^y/i.test(ans)) {
673
- console.log("\n Aborted. Nothing was changed.\n");
753
+ console.log("└ Nothing was changed.");
754
+ console.log("");
674
755
  return;
675
756
  }
676
757
  }
677
758
 
759
+ console.log("│");
678
760
  runUninstall(targets, isFull);
679
761
  }
680
762
 
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * "Using Niro": what a coding agent should do with Niro beyond code questions. Written into every instruction text a
5
+ * release without the cloud installs (the niro-skill, the CLAUDE.md section, the marked sections for Codex, Gemini
6
+ * CLI, Copilot CLI, Factory, OpenCode and Windsurf, and the Cursor, Cline and Kiro rules), and said by the released
7
+ * engine in its MCP instructions, which every one of the 14 assistants reads (niro-local
8
+ * `src/niro_local/mcp/using_niro.py`).
9
+ *
10
+ * Why (Niraj, 2026-11-11): 92 pages ship with niro (`niro guide`), and an agent asked a question about Niro never
11
+ * opened one, because no text it read named them; "this has to happen automatically part of the installation".
12
+ * How an agent finds a page (Niraj, 2026-11-11, "agreed"): it reads the list with its one-line descriptions and
13
+ * picks by meaning, because agents match meaning themselves, niro-cli carries no model, and the pages work offline.
14
+ * `--search` is named only for an exact message or word. Decision 01M75G5K5NYJDT1EWM3K2V3PTT.
15
+ *
16
+ * ONE text in each repository, equal word for word: test/usingNiro.test.js reads niro-local's copy and niro-local's
17
+ * tests/test_using_niro.py reads this one. Kept to a template literal with no `${}` so a reader outside JavaScript
18
+ * can take it out of this file. No blank line inside it: a marked section ends at its first blank line when an
19
+ * upgrade or an uninstall has to cut out a text it no longer knows word for word (teardown.js).
20
+ * Every command it names is checked against the CLI by that test.
21
+ */
22
+ const USING_NIRO = `Using Niro:
23
+ - Code questions: use the niro tools, as above.
24
+ - A "how do I" question about Niro: run \`niro guide\`, which lists every page with one line on what it answers. Pick the page whose line fits the question, run \`niro guide <page>\`, and answer from it. The pages ship with niro and work offline. For work on code: playbook-debug-exception, playbook-change-safely, playbook-review-a-change, playbook-test-a-change.
25
+ - A Niro error message, or an exact word such as an error code: \`niro guide --search "<the message>"\` names the page that explains it.
26
+ - Code in a language, framework or tool Niro does not read: offer to ask Niro's team for it with the request_integration tool, which sends nothing until the user says yes. In a terminal: \`niro request-integration <name>\`.
27
+ - Managing Niro, in the terminal:
28
+ - index another folder: \`niro init <folder>\`, only when the user asks or says yes
29
+ - rebuild a folder's graph: \`niro build <folder>\`
30
+ - what is indexed here: \`niro status\`
31
+ - the plan and its line limit: \`niro plan\`
32
+ - what Niro did for the user: \`niro impact\`
33
+ - connect calls between services: \`niro route-rule unconnected\`, after reading \`niro guide configuring-a-folder\`
34
+ - anything else: \`niro guide\``;
35
+
36
+ /** The commands USING_NIRO names, each a real `niro` command (checked by test/usingNiro.test.js against src/index.js). */
37
+ const USING_NIRO_COMMANDS = ["guide", "request-integration", "init", "build", "status", "plan", "impact", "route-rule"];
38
+
39
+ module.exports = { USING_NIRO, USING_NIRO_COMMANDS };
@@ -1,162 +0,0 @@
1
- # Working on branches with Niro
2
-
3
- Your team decides what Niro indexes: you create projects, and each project
4
- covers the repos and branches you choose. One project might index main,
5
- another might index a develop or release branch. That is up to you.
6
-
7
- When you ask a question, Niro picks the project that matches your repo and
8
- the branch you are currently on. This page is about the moment when NO
9
- project matches: you created a new branch and changed code that no project
10
- has indexed yet. Two minutes to read.
11
-
12
- ## The short version
13
-
14
- There is one command to remember:
15
-
16
- niro new-temp-project
17
-
18
- Run it in your repo folder when Niro asks for it. That is all.
19
-
20
- ## What you will see
21
-
22
- You create a branch that no project covers and start changing code.
23
- Then you ask your AI assistant something about the code.
24
-
25
- Niro notices that no project has indexed this branch. Instead of giving
26
- you a wrong answer based on some other branch, it replies with something
27
- like:
28
-
29
- I cannot answer this safely. Your branch has diverged
30
- from the branch Niro indexed.
31
-
32
- Your assistant will offer to fix this by running:
33
-
34
- niro new-temp-project
35
-
36
- Say yes, or run it yourself in the repo folder. It takes a few seconds.
37
-
38
- ## What the command does
39
-
40
- It creates your own private copy of the project inside Niro.
41
-
42
- - The copy follows your branch, including changes you have not committed.
43
- - Every save updates the copy, as long as "niro watch" is running.
44
- - Your original project is not changed. It keeps its own index.
45
- - The copy is fully separate and disposable.
46
-
47
- From then on, answers come from YOUR code.
48
-
49
- The commands follow too. `niro status` and `niro build` act on the copy for the branch you are on,
50
- not on the shared project, so "is my index up to date" and "rebuild it" mean what you expect while
51
- you are working on a branch. Name a project explicitly if you want a different one.
52
-
53
- `niro delete` and `niro remove --project` will not delete a private copy. They send you to
54
- `niro discard-temp-project`, which removes the copy AND points your folder back at the shared
55
- project. Deleting it any other way leaves your folder aimed at something that is gone.
56
-
57
- ## Switching branches
58
-
59
- Nothing to do. Niro checks which branch you are on for every question.
60
-
61
- - On your new branch: answers come from your private copy.
62
- - On a branch one of your projects already indexes: answers come
63
- from that project.
64
-
65
- ## When your work is merged
66
-
67
- Run this in the repo folder:
68
-
69
- niro discard-temp-project
70
-
71
- It deletes your private copy. Your local files are never touched.
72
- If you forget, Niro deletes unused copies by itself after two weeks.
73
-
74
- ## Tidying up copies that were deleted for you
75
-
76
- When a copy goes after two weeks, or somebody deletes it in the web app,
77
- your machine is not told. The folder is left pointing at a copy that is
78
- no longer there. Nothing breaks, but "niro new-temp-project" in another
79
- worktree of the same repo will say the slot is already taken.
80
-
81
- Run this from anywhere to sort it out:
82
-
83
- niro doctor sync-registry
84
-
85
- You will rarely need to type it. The background watcher runs the same
86
- check every six hours by itself.
87
-
88
- It sorts out three things, and treats them differently:
89
-
90
- - A folder pointing at a private copy that has gone goes back to the
91
- original project, and carries on syncing.
92
- - A folder pointing at a project that has gone is only unregistered when
93
- the repo has gone too. A project can be deleted while its repos are
94
- moved somewhere else, and that folder is still syncing perfectly well.
95
- - A ".niro-project" file naming something the server does not know is
96
- repaired where possible. That file can hold a project's NAME rather
97
- than its id, so a renamed project looks exactly like a deleted one.
98
- Niro checks your folder's git remote instead: if that points at one
99
- project, the file is rewritten to that project's id, which a future
100
- rename cannot break. If the remote points at nothing either, the file
101
- names nothing and is removed.
102
-
103
- Not if the file is committed to git, though. Then it is shared with
104
- everyone who clones the repo, and a project that means nothing to you
105
- may be a colleague's. Niro tells you and leaves the choice to you.
106
- "niro info" mentions it too, when you are standing in that folder.
107
-
108
- Copies that are still there are left alone, and so is anything it could
109
- not check, so a dropped connection never costs you a working folder. Add
110
- "--dry-run" to see the list without changing anything.
111
-
112
- ## When you run out of private copies
113
-
114
- You get a limited number of private copies of each project. When you hit
115
- the limit, Niro says which branches are holding them, and the CLI adds
116
- where each one is checked out on this machine:
117
-
118
- You already have 5 working copies of this project, which is
119
- the limit (5). They are for: feature/a, feature/b, ...
120
-
121
- You have 5 working copies of this project on this machine:
122
- feature/a /home/you/work/service
123
- feature/b /home/you/work/service-wt2
124
-
125
- Run "niro discard-temp-project" in one you have finished with, then try
126
- again. Copies nobody has touched for a while are cleared automatically,
127
- so this usually sorts itself out.
128
-
129
- ## A feature that spans several repos
130
-
131
- Run "niro new-temp-project" in each repo folder you branch.
132
- All of them join the same private copy, so questions that cross repos
133
- (for example: what breaks in service B if I change service A?) see all
134
- of your changes together.
135
-
136
- ## Common questions
137
-
138
- Does this change my original project?
139
- No. The original project and its index stay exactly as they were.
140
- The copy is separate; deleting it later loses nothing.
141
-
142
- Does this work with my AI assistant and my git host?
143
- Yes. Any assistant that supports MCP (Claude Code, Cursor, Windsurf,
144
- Codex, and others) and any git server (GitHub, GitLab, Bitbucket,
145
- self-hosted).
146
-
147
- Is creating a copy slow?
148
- No. Niro copies the existing index instead of re-reading your code.
149
- It normally takes seconds.
150
-
151
- I just created a branch and have not changed anything. Will Niro nag me?
152
- No. Your code is still identical to the branch you started from, so Niro
153
- answers normally. It only steps in once your code actually differs.
154
-
155
- We already have projects for main and develop. Do I need this?
156
- Not while you are on main or develop: Niro routes to the matching project
157
- by itself. You only need a temporary project for a branch that none of
158
- your projects cover.
159
-
160
- What if I really want an answer from the shared project while on my branch?
161
- Tell your assistant so. The answer will be clearly labeled as describing
162
- the indexed branch, not your code.