@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.
- package/CHANGELOG.md +149 -0
- package/README.md +123 -302
- package/bin/niro.js +12 -1
- package/docs/agent-antigravity.md +37 -0
- package/docs/agent-claude-code.md +48 -0
- package/docs/agent-claude-desktop.md +47 -0
- package/docs/agent-cline.md +42 -0
- package/docs/agent-codex.md +45 -0
- package/docs/agent-copilot-cli.md +42 -0
- package/docs/agent-cursor.md +44 -0
- package/docs/agent-factory.md +42 -0
- package/docs/agent-gemini-cli.md +42 -0
- package/docs/agent-goose.md +42 -0
- package/docs/agent-kiro.md +37 -0
- package/docs/agent-opencode.md +37 -0
- package/docs/agent-vscode.md +45 -0
- package/docs/agent-windsurf.md +38 -0
- package/docs/answer-shapes.md +69 -0
- package/docs/builds-and-updates.md +68 -0
- package/docs/cloud-README.md +372 -0
- package/docs/configuring-a-folder.md +51 -0
- package/docs/connect-ci.md +24 -0
- package/docs/connect-overview.md +20 -0
- package/docs/connect-review-bots.md +17 -0
- package/docs/connect-team-server.md +12 -0
- package/docs/connect-terminal.md +49 -0
- package/docs/connect-web-agents.md +9 -0
- package/docs/connect-your-ai-assistant.md +59 -175
- package/docs/coverage-audit-and-consent.md +51 -0
- package/docs/evaluate-what-niro-did.md +271 -0
- package/docs/faq-assistants-and-mcp.md +122 -0
- package/docs/faq-feedback-and-receipts.md +33 -0
- package/docs/faq-indexing.md +127 -0
- package/docs/faq-install-and-setup.md +103 -0
- package/docs/faq-licence-and-plans.md +86 -0
- package/docs/faq-login-and-account.md +69 -0
- package/docs/faq-offline.md +34 -0
- package/docs/faq-performance.md +33 -0
- package/docs/faq-privacy-and-security.md +113 -0
- package/docs/faq-uninstall.md +47 -0
- package/docs/faq-updates.md +59 -0
- package/docs/faq-worktrees-and-branches.md +59 -0
- package/docs/faq-wrong-or-missing-answers.md +275 -0
- package/docs/hosted-only.md +33 -0
- package/docs/how-the-graph-works.md +47 -0
- package/docs/index.md +157 -0
- package/docs/licence-and-plans.md +72 -0
- package/docs/lifecycle-debug.md +48 -0
- package/docs/lifecycle-onboarding.md +32 -0
- package/docs/lifecycle-overview.md +77 -0
- package/docs/lifecycle-plan-and-design.md +44 -0
- package/docs/lifecycle-review.md +30 -0
- package/docs/lifecycle-security-and-privacy.md +35 -0
- package/docs/lifecycle-test.md +28 -0
- package/docs/lifecycle-write-code.md +35 -0
- package/docs/llms-full.txt +8015 -0
- package/docs/llms.txt +126 -0
- package/docs/playbook-add-a-feature.md +108 -0
- package/docs/playbook-change-safely.md +93 -0
- package/docs/playbook-data-and-events.md +107 -0
- package/docs/playbook-debug-after-a-change.md +96 -0
- package/docs/playbook-debug-config.md +95 -0
- package/docs/playbook-debug-exception.md +124 -0
- package/docs/playbook-debug-failing-service-call.md +119 -0
- package/docs/playbook-debug-slow-path.md +80 -0
- package/docs/playbook-debug-wrong-value.md +137 -0
- package/docs/playbook-onboard.md +95 -0
- package/docs/playbook-review-a-change.md +72 -0
- package/docs/playbook-test-a-change.md +73 -0
- package/docs/reference-cli.md +113 -0
- package/docs/reference-config-files.md +42 -0
- package/docs/reference-engine-commands.md +56 -0
- package/docs/reference-environment-variables.md +89 -0
- package/docs/reference-exit-codes.md +67 -0
- package/docs/reference-limits.md +75 -0
- package/docs/reference-mcp-tools.md +326 -0
- package/docs/reference-messages.md +180 -0
- package/docs/secrets-and-masking.md +48 -0
- package/docs/supported-c.md +130 -0
- package/docs/supported-cpp.md +157 -0
- package/docs/supported-csharp.md +211 -0
- package/docs/supported-go.md +258 -0
- package/docs/supported-java.md +226 -0
- package/docs/supported-javascript.md +223 -0
- package/docs/supported-kotlin.md +124 -0
- package/docs/supported-overview.md +89 -0
- package/docs/supported-php.md +157 -0
- package/docs/supported-python.md +224 -0
- package/docs/supported-ruby.md +162 -0
- package/docs/supported-rust.md +150 -0
- package/docs/supported-swift.md +133 -0
- package/docs/supported-typescript.md +220 -0
- package/docs/tokens-speed-and-accuracy.md +99 -0
- package/docs/troubleshooting.md +42 -0
- package/docs/uninstall.md +74 -0
- package/docs/what-leaves-your-machine.md +71 -0
- package/docs/where-things-are-stored.md +59 -0
- package/docs/worktrees-and-branches.md +55 -0
- package/package.json +5 -3
- package/src/commands/add-repo.js +5 -0
- package/src/commands/auditSend.js +67 -0
- package/src/commands/build.js +6 -0
- package/src/commands/docsSend.js +73 -0
- package/src/commands/engine.js +120 -0
- package/src/commands/engineRoutes.js +105 -0
- package/src/commands/guide.js +254 -0
- package/src/commands/init.js +16 -7
- package/src/commands/initLocal.js +96 -0
- package/src/commands/journalSend.js +47 -0
- package/src/commands/login.js +89 -12
- package/src/commands/mcp.js +22 -6
- package/src/commands/plan.js +99 -0
- package/src/commands/repoPush.js +33 -0
- package/src/commands/requestIntegration.js +38 -0
- package/src/commands/share.js +56 -0
- package/src/commands/start.js +451 -371
- package/src/commands/status.js +57 -0
- package/src/commands/takeover.js +6 -0
- package/src/commands/uninstall.js +180 -0
- package/src/commands/upgrade.js +38 -8
- package/src/commands/watch.js +420 -6
- package/src/index.js +174 -3
- package/src/lib/agentApi.js +32 -1
- package/src/lib/api.js +20 -6
- package/src/lib/auditUpload.js +293 -0
- package/src/lib/branchDrift.js +149 -0
- package/src/lib/choose.js +116 -0
- package/src/lib/cloudGuard.js +74 -0
- package/src/lib/credentials.js +2 -2
- package/src/lib/docsMarked.js +55 -0
- package/src/lib/docsState.js +213 -0
- package/src/lib/docsUpload.js +481 -0
- package/src/lib/engine.js +619 -0
- package/src/lib/folderSync.js +2 -1
- package/src/lib/graphCheck.js +40 -0
- package/src/lib/guide.js +293 -0
- package/src/lib/guideSearch.js +252 -0
- package/src/lib/integrationRequest.js +89 -0
- package/src/lib/journalUpload.js +147 -0
- package/src/lib/languages.js +129 -0
- package/src/lib/lease.js +375 -0
- package/src/lib/localBuild.js +285 -0
- package/src/lib/localEngine.js +210 -0
- package/src/lib/localOnboard.js +124 -0
- package/src/lib/localWorktrees.js +243 -0
- package/src/lib/machineCheck.js +58 -0
- package/src/lib/mcpClients.js +10 -1
- package/src/lib/migrationNotice.js +118 -0
- package/src/lib/modelFetch.js +153 -0
- package/src/lib/nestedOutput.js +23 -9
- package/src/lib/onboardingLog.js +34 -0
- package/src/lib/planUsage.js +165 -0
- package/src/lib/progressBar.js +46 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/redact.js +155 -0
- package/src/lib/release.js +33 -0
- package/src/lib/repoPush.js +495 -0
- package/src/lib/secret.js +1 -1
- package/src/lib/service.js +372 -63
- package/src/lib/shareCode.js +367 -0
- package/src/lib/shareExclusions.js +114 -0
- package/src/lib/shareFiles.js +129 -0
- package/src/lib/spinner.js +9 -5
- package/src/lib/watchDaemon.js +33 -10
- package/src/lib/watchState.js +12 -1
- package/src/lib/windowsPaths.js +67 -0
- package/src/mcp/lineCheck.js +382 -0
- package/src/mcp/lineName.js +92 -0
- package/src/mcp/localBackend.js +751 -0
- package/src/mcp/localMode.js +383 -0
- package/src/mcp/localSkill.js +189 -0
- package/src/mcp/server.js +610 -79
- package/src/mcp/setup.js +519 -131
- package/src/mcp/shellSearchHook.js +509 -0
- package/src/mcp/teardown.js +173 -91
- package/src/mcp/usingNiro.js +39 -0
- package/docs/working-on-branches.md +0 -162
package/src/mcp/setup.js
CHANGED
|
@@ -21,7 +21,13 @@ const path = require("path");
|
|
|
21
21
|
const readline = require("readline");
|
|
22
22
|
const { execSync } = require("child_process");
|
|
23
23
|
const config = require("../lib/config");
|
|
24
|
+
const localMode = require("./localMode");
|
|
25
|
+
const shellSearchHook = require("./shellSearchHook");
|
|
24
26
|
const { formatPrompt } = require("../lib/prompt");
|
|
27
|
+
const { choose } = require("../lib/choose");
|
|
28
|
+
const color = require("../lib/color");
|
|
29
|
+
/** Inside `niro start` these lines sit on its rail ("│ "), so they take the rail's indent. */
|
|
30
|
+
const IND = process.env.NIRO_OUTPUT_PREFIX ? " " : " ";
|
|
25
31
|
|
|
26
32
|
/** Cross-platform user home (~). */
|
|
27
33
|
function userHome() {
|
|
@@ -35,6 +41,7 @@ function userHome() {
|
|
|
35
41
|
* (which removes the section) can never drift apart.
|
|
36
42
|
*/
|
|
37
43
|
const NIRO_MD_MARKER = "<!-- niro-mcp -->";
|
|
44
|
+
const { USING_NIRO } = require("./usingNiro");
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* The exact niro sections appended to each markdown config file. These are the single source
|
|
@@ -73,10 +80,83 @@ const NIRO_WINDSURF_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP cod
|
|
|
73
80
|
* copilot-instructions.md, Factory's AGENTS.md, OpenCode's AGENTS.md) — one wording, reused
|
|
74
81
|
* across those destination files (teardown matches it per-file, so reuse is safe).
|
|
75
82
|
*/
|
|
76
|
-
|
|
83
|
+
/**
|
|
84
|
+
* One line telling an agent with a terminal that Niro's own pages are readable offline (`niro guide`, in both
|
|
85
|
+
* releases). Given to the agents that get only a rule file and can run commands: Cursor, Cline, Kiro, and the
|
|
86
|
+
* generic section (Gemini CLI, Copilot CLI, Factory, OpenCode). Without backticks, so the Cursor rule can carry it.
|
|
87
|
+
*/
|
|
88
|
+
const NIRO_GUIDE_SENTENCE = "For a Niro error message or a question about Niro itself, run niro guide --search <the message> in the terminal and answer from the page it names.";
|
|
89
|
+
const NIRO_GENERIC_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools are available (call chains, blast radius, impact analysis, source lookup). Prefer them over grep/read for questions about this codebase. Before adding or writing any new code, use find_reusable_code to check for reusable code. If the niro tools are not present or the repo is not indexed, use native tools.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_IMPACT_BOTH_WAYS_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n${NIRO_GUIDE_SENTENCE}\n`;
|
|
77
90
|
|
|
78
91
|
/** All niro markdown sections — the uninstaller tries each for an exact removal. */
|
|
79
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The instruction text for a release without the cloud: drops the rules that name what only the
|
|
94
|
+
* cloud has, the mandatory find_reusable_code call (an engine offers it only after a --llm build)
|
|
95
|
+
* and the temporary-project rule (`niro new-temp-project` is a cloud command). Every tool it names
|
|
96
|
+
* must be one the engine serves (test/releaseToolNames.test.js): the cloud's resolve_fqn is the
|
|
97
|
+
* engine's resolve_name, and the Cursor rule's cloud-only routes (explain_code,
|
|
98
|
+
* trace_execution_path, find_known_issues, get_state_at_entry, and the comments about hidden
|
|
99
|
+
* tools) are left out.
|
|
100
|
+
*/
|
|
101
|
+
function localTextBeforeUsingNiro(text) {
|
|
102
|
+
return text
|
|
103
|
+
.split("resolve_fqn").join("resolve_name")
|
|
104
|
+
.replace(/\n<!-- [^\n]*\(hidden MCP tool\)[^\n]*-->/g, "")
|
|
105
|
+
.replace(/explain_code \(AI docs\) \| /g, "")
|
|
106
|
+
.replace(/ \| trace_execution_path \(A to B\)/g, "")
|
|
107
|
+
.replace(/\n- debug: find_known_issues \| get_state_at_entry/g, "")
|
|
108
|
+
.split(`\n${NIRO_MULTI_REPO_SENTENCE}`).join("")
|
|
109
|
+
.replace(/\n[^\n]*before writing\/adding any code: find_reusable_code[^\n]*/g, "")
|
|
110
|
+
.replace(/\n[ \t]*Before (?:adding\/writing|adding or writing|writing or adding|writing)(?: any)? new code,[^.]*find_reusable_code[^.]*\.[ \t]*(?=\n)/g, "")
|
|
111
|
+
.replace(/[ \t]*Before (?:adding\/writing|adding or writing|writing or adding|writing)(?: any)? new code,[^.]*find_reusable_code[^.]*\.[ \t]*/g, "")
|
|
112
|
+
.replace(/[ \t]*If find_reusable_code[^.]*\./g, "")
|
|
113
|
+
.replace(/(\S) +\n/g, "$1\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A release text with "Using Niro" in it (src/mcp/usingNiro.js): in place of the one guide sentence where a text has
|
|
118
|
+
* it (the generic section, the Cursor, Cline and Kiro rules), else at the end of a marked section (Claude Code's
|
|
119
|
+
* CLAUDE.md through localMode.js, Codex's AGENTS.md, Windsurf's global rules). A plain sentence passes unchanged.
|
|
120
|
+
*/
|
|
121
|
+
function withUsingNiro(text) {
|
|
122
|
+
if (text.includes(NIRO_GUIDE_SENTENCE)) return text.split(NIRO_GUIDE_SENTENCE).join(USING_NIRO);
|
|
123
|
+
if (text.startsWith(`\n${NIRO_MD_MARKER}\n`)) return `${text}${USING_NIRO}\n`;
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The text a release without the cloud installs: the engine's tool names, no cloud-only rules, and Using Niro. */
|
|
128
|
+
function localText(text) {
|
|
129
|
+
return withUsingNiro(localTextBeforeUsingNiro(text));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Pick the wording for this release. */
|
|
133
|
+
function forRelease(text) {
|
|
134
|
+
return require("../lib/release").cloud ? text : localText(text);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const CLOUD_MD_SECTIONS = [NIRO_CLAUDE_MD_SECTION, NIRO_CODEX_MD_SECTION, NIRO_WINDSURF_MD_SECTION, NIRO_GENERIC_MD_SECTION];
|
|
138
|
+
// Every wording, so teardown and a refresh remove a section whichever release wrote it: the cloud's, this release's,
|
|
139
|
+
// and this release's before Using Niro was added (9255e24), which `niro upgrade` must replace exactly.
|
|
140
|
+
const NIRO_MD_SECTIONS = [
|
|
141
|
+
...CLOUD_MD_SECTIONS,
|
|
142
|
+
...CLOUD_MD_SECTIONS.map(localText),
|
|
143
|
+
...CLOUD_MD_SECTIONS.map(localTextBeforeUsingNiro),
|
|
144
|
+
];
|
|
145
|
+
/**
|
|
146
|
+
* Every section the uninstaller removes exactly: the cloud ones above (both wordings) and local mode's
|
|
147
|
+
* (src/mcp/localMode.js). Kept apart because the cloud sections all carry the cloud rules word for word,
|
|
148
|
+
* which the local one, naming the engine's tools, deliberately does not.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* The sections as the installer wrote them before NIRO_GUIDE_SENTENCE was added (3a585c8, the release before
|
|
152
|
+
* `niro guide`), so a refresh and the uninstaller remove those exactly too, instead of falling back to a
|
|
153
|
+
* bounded strip. Derived by taking the sentence out, and checked against the 3a585c8 texts in
|
|
154
|
+
* test/fixtures/md-sections-3a585c8.json.
|
|
155
|
+
*/
|
|
156
|
+
const BEFORE_GUIDE_MD_SECTIONS = NIRO_MD_SECTIONS
|
|
157
|
+
.map((section) => section.split(`${NIRO_GUIDE_SENTENCE}\n`).join(""))
|
|
158
|
+
.filter((section) => !NIRO_MD_SECTIONS.includes(section));
|
|
159
|
+
const NIRO_ALL_MD_SECTIONS = [...NIRO_MD_SECTIONS, ...BEFORE_GUIDE_MD_SECTIONS, ...localMode.LOCAL_CLAUDE_MD_SECTIONS];
|
|
80
160
|
|
|
81
161
|
/**
|
|
82
162
|
* Upsert a niro markdown section into an agent-rules file's existing content. Returns {content, action}:
|
|
@@ -84,20 +164,20 @@ const NIRO_MD_SECTIONS = [NIRO_CLAUDE_MD_SECTION, NIRO_CODEX_MD_SECTION, NIRO_WI
|
|
|
84
164
|
* - no niro marker at all -> {existing + section, "added"}
|
|
85
165
|
* - marker present but content DIFFERS -> strip the old bounded niro block, append fresh, "refreshed"
|
|
86
166
|
* The "refreshed" path fixes the review bug where an existing user never received UPDATED instructions (the
|
|
87
|
-
* new temp-project sentence) because the installer skipped whenever the marker was present. The
|
|
88
|
-
*
|
|
89
|
-
*
|
|
167
|
+
* new temp-project sentence) because the installer skipped whenever the marker was present. The old block is
|
|
168
|
+
* removed as teardown removes it (removeMarkerSection): a known section text exactly, otherwise only the lines a
|
|
169
|
+
* known section holds, up to the first blank line, so every line a user wrote below it survives.
|
|
90
170
|
*/
|
|
91
171
|
function upsertNiroMarkerSection(existing, section) {
|
|
92
172
|
if (existing.includes(section)) return { content: existing, action: "unchanged" };
|
|
93
173
|
if (!existing.includes(NIRO_MD_MARKER)) return { content: existing + section, action: "added" };
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
return { content:
|
|
174
|
+
// The old section comes out the way the uninstaller takes it out: every section text this installer has
|
|
175
|
+
// written, exactly, and only for a text it does not know, the lines of the marker's block (up to the first blank
|
|
176
|
+
// line) that a known section holds. Until 2026-11-10 this cut everything up to the next "## " heading, so a
|
|
177
|
+
// user's note written under the section (no heading, a "#" or "###" heading) was deleted on every refresh
|
|
178
|
+
// (check of f1abc9f).
|
|
179
|
+
const { content } = require("./teardown").removeMarkerSection(existing, NIRO_MD_MARKER, NIRO_ALL_MD_SECTIONS);
|
|
180
|
+
return { content: content + section, action: "refreshed" };
|
|
101
181
|
}
|
|
102
182
|
|
|
103
183
|
/**
|
|
@@ -121,7 +201,10 @@ function isNiroUserPromptHook(h) {
|
|
|
121
201
|
const cmds = [];
|
|
122
202
|
if (h && Array.isArray(h.hooks)) h.hooks.forEach((i) => i && typeof i.command === "string" && cmds.push(i.command));
|
|
123
203
|
if (h && typeof h.command === "string") cmds.push(h.command);
|
|
124
|
-
|
|
204
|
+
// The local reminder (localMode.js) has neither `.niro-project` nor, with LLM documentation off, find_reusable_code:
|
|
205
|
+
// its generated opening is what names it.
|
|
206
|
+
return cmds.some((c) => c.includes(".niro-project") || c.includes("find_reusable_code") || c.includes("`niro` skill")
|
|
207
|
+
|| c.includes(NIRO_USER_PROMPT_GENERATED_MARKERS[0]));
|
|
125
208
|
}
|
|
126
209
|
|
|
127
210
|
/**
|
|
@@ -149,6 +232,18 @@ function isNiroStopHook(h) {
|
|
|
149
232
|
return cmds.some((c) => c.includes("mcp receipt --hook"));
|
|
150
233
|
}
|
|
151
234
|
|
|
235
|
+
/**
|
|
236
|
+
* True if `h` is niro's line-check Stop hook. Keyed on the exact subcommand it runs, so a developer's
|
|
237
|
+
* own Stop hook is never matched. Deliberately not isNiroStopHook, which finds the retired hook that
|
|
238
|
+
* printed the line itself so an install can delete it; this one is meant to stay.
|
|
239
|
+
*/
|
|
240
|
+
function isNiroLineCheckHook(h) {
|
|
241
|
+
const cmds = [];
|
|
242
|
+
if (h && Array.isArray(h.hooks)) h.hooks.forEach((i) => i && typeof i.command === "string" && cmds.push(i.command));
|
|
243
|
+
if (h && typeof h.command === "string") cmds.push(h.command);
|
|
244
|
+
return cmds.some((c) => c.includes(NIRO_LINE_CHECK_SUBCOMMAND));
|
|
245
|
+
}
|
|
246
|
+
|
|
152
247
|
/**
|
|
153
248
|
* True if `h` is niro's PreToolUse explore-first reminder entry (matcher Grep|Glob|Read). Keyed on the
|
|
154
249
|
* unique `niro-explore-` marker-file prefix so a foreign PreToolUse hook on the same matcher is never
|
|
@@ -174,6 +269,21 @@ function isNiroImpactHook(h) {
|
|
|
174
269
|
return cmds.some((c) => c.includes("niro-impact-"));
|
|
175
270
|
}
|
|
176
271
|
|
|
272
|
+
/**
|
|
273
|
+
* The environment every assistant starts `niro mcp serve` with: the key when there is no `niro login` session, and the
|
|
274
|
+
* bridge's server address. With the cloud off (src/lib/release.js) it also carries NIRO_MCP_MODE=local, the setting
|
|
275
|
+
* Claude Code is registered with (setupClaudeCode), so every assistant's bridge answers from the engine on this
|
|
276
|
+
* machine. Without it, an assistant started with no login stopped at the bridge's credential check before the engine
|
|
277
|
+
* was started (13 of 14 clients, the checker's note 1 on fix/cli-small, 2026-11-06).
|
|
278
|
+
*/
|
|
279
|
+
function bridgeEnv(apiKey, mcpServerUrl) {
|
|
280
|
+
return {
|
|
281
|
+
...(apiKey ? { NIRO_API_KEY: apiKey } : {}),
|
|
282
|
+
NIRO_MCP_SERVER_URL: mcpServerUrl,
|
|
283
|
+
...(require("../lib/release").cloud ? {} : { NIRO_MCP_MODE: "local" }),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
177
287
|
/**
|
|
178
288
|
* Writes a niro-managed skill/rule file, refreshing it when the generated content has
|
|
179
289
|
* changed. These files are GENERATED (not hand-edited), so refresh-on-change is correct:
|
|
@@ -183,7 +293,11 @@ function isNiroImpactHook(h) {
|
|
|
183
293
|
* `label` is a short human name used in the log line (e.g. "niro skill").
|
|
184
294
|
* Returns one of: "added" | "unchanged" | "updated".
|
|
185
295
|
*/
|
|
296
|
+
/** Skills written or refreshed during this run, so the summary can say where they went. */
|
|
297
|
+
const installedSkills = [];
|
|
298
|
+
|
|
186
299
|
function writeManagedSkill(absPath, content, label) {
|
|
300
|
+
installedSkills.push({ label, dir: path.dirname(absPath) });
|
|
187
301
|
if (!fs.existsSync(absPath)) {
|
|
188
302
|
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
189
303
|
fs.writeFileSync(absPath, content);
|
|
@@ -249,7 +363,7 @@ const CLIENTS = {
|
|
|
249
363
|
type: "stdio",
|
|
250
364
|
command: "niro",
|
|
251
365
|
args: ["mcp", "serve"],
|
|
252
|
-
env: apiKey
|
|
366
|
+
env: bridgeEnv(apiKey, mcpServerUrl),
|
|
253
367
|
}),
|
|
254
368
|
},
|
|
255
369
|
"gemini-cli": {
|
|
@@ -277,9 +391,7 @@ const CLIENTS = {
|
|
|
277
391
|
type: "local",
|
|
278
392
|
command: ["niro", "mcp", "serve"],
|
|
279
393
|
enabled: true,
|
|
280
|
-
environment: apiKey
|
|
281
|
-
? { NIRO_API_KEY: apiKey, NIRO_MCP_SERVER_URL: mcpServerUrl }
|
|
282
|
-
: { NIRO_MCP_SERVER_URL: mcpServerUrl },
|
|
394
|
+
environment: bridgeEnv(apiKey, mcpServerUrl),
|
|
283
395
|
}),
|
|
284
396
|
},
|
|
285
397
|
cline: {
|
|
@@ -300,7 +412,7 @@ const CLIENTS = {
|
|
|
300
412
|
buildEntry: (apiKey, mcpServerUrl) => ({
|
|
301
413
|
command: "niro",
|
|
302
414
|
args: ["mcp", "serve"],
|
|
303
|
-
env: apiKey
|
|
415
|
+
env: bridgeEnv(apiKey, mcpServerUrl),
|
|
304
416
|
disabled: false,
|
|
305
417
|
autoApprove: [],
|
|
306
418
|
}),
|
|
@@ -312,7 +424,7 @@ const CLIENTS = {
|
|
|
312
424
|
buildEntry: (apiKey, mcpServerUrl) => ({
|
|
313
425
|
command: "niro",
|
|
314
426
|
args: ["mcp", "serve"],
|
|
315
|
-
env: apiKey
|
|
427
|
+
env: bridgeEnv(apiKey, mcpServerUrl),
|
|
316
428
|
disabled: false,
|
|
317
429
|
// Kiro's autoApprove supports a "*" wildcard for "all tools on this server" — safe here
|
|
318
430
|
// because every niro tool is read-only.
|
|
@@ -334,7 +446,7 @@ const CLIENTS = {
|
|
|
334
446
|
buildEntry: (apiKey, mcpServerUrl) => ({
|
|
335
447
|
command: "niro",
|
|
336
448
|
args: ["mcp", "serve"],
|
|
337
|
-
env: apiKey
|
|
449
|
+
env: bridgeEnv(apiKey, mcpServerUrl),
|
|
338
450
|
}),
|
|
339
451
|
},
|
|
340
452
|
};
|
|
@@ -556,13 +668,6 @@ function hasShellMetacharacters(value) {
|
|
|
556
668
|
return /[\s;&|`$()<>"'\\]/.test(value || "");
|
|
557
669
|
}
|
|
558
670
|
|
|
559
|
-
function printHeader() {
|
|
560
|
-
console.log("");
|
|
561
|
-
console.log(" Niro MCP Server Setup");
|
|
562
|
-
console.log(" ---------------------");
|
|
563
|
-
console.log("");
|
|
564
|
-
}
|
|
565
|
-
|
|
566
671
|
/** How many clients to name before summarising the rest. */
|
|
567
672
|
const CLIENTS_SHOWN = 5;
|
|
568
673
|
|
|
@@ -588,6 +693,22 @@ function runClientCommand(command) {
|
|
|
588
693
|
* a question nearly everyone answers with the default. The full list is one flag away, and
|
|
589
694
|
* anyone hunting for a client they cannot see is exactly the person who will use it.
|
|
590
695
|
*/
|
|
696
|
+
/** The picker's last row: an MCP client not in CLIENTS. */
|
|
697
|
+
const OTHER = "other";
|
|
698
|
+
|
|
699
|
+
/** The stdio entry every JSON client gets (setupJsonConfig's default), to paste by hand. */
|
|
700
|
+
function printOtherConfig(apiKey, mcpServerUrl) {
|
|
701
|
+
const env = bridgeEnv(apiKey, mcpServerUrl);
|
|
702
|
+
const entry = { mcpServers: { niro: { command: "niro", args: ["mcp", "serve"], env } } };
|
|
703
|
+
console.log(`${IND}Add this to your assistant's MCP settings, then restart it:`);
|
|
704
|
+
JSON.stringify(entry, null, 2).split("\n").forEach((l) => console.log(color.dim(`${IND} ${l}`)));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** "claude-code, cursor" or "claude-code cursor" → unique ids in the order typed. */
|
|
708
|
+
function parseClients(input) {
|
|
709
|
+
return [...new Set(String(input || "").split(/[\s,]+/).filter(Boolean))];
|
|
710
|
+
}
|
|
711
|
+
|
|
591
712
|
function printClients({ all = false } = {}) {
|
|
592
713
|
const entries = Object.entries(CLIENTS);
|
|
593
714
|
const shown = all ? entries : entries.slice(0, CLIENTS_SHOWN);
|
|
@@ -615,10 +736,12 @@ function registeredServerUrl(getOutput) {
|
|
|
615
736
|
* "keep" registered and already pointing at the wanted URL
|
|
616
737
|
* "replace" registered, but at another URL or in a shape this cannot read: remove, then add
|
|
617
738
|
*/
|
|
618
|
-
function planClaudeRegistration({ existingOutput, wantedUrl }) {
|
|
739
|
+
function planClaudeRegistration({ existingOutput, wantedUrl, mode = "cloud" }) {
|
|
619
740
|
if (existingOutput == null) return "add";
|
|
741
|
+
const local = /NIRO_MCP_MODE=local\b/.test(String(existingOutput));
|
|
742
|
+
if (mode === "local") return local ? "keep" : "replace";
|
|
620
743
|
const url = registeredServerUrl(existingOutput);
|
|
621
|
-
return url && url === wantedUrl ? "keep" : "replace";
|
|
744
|
+
return !local && url && url === wantedUrl ? "keep" : "replace";
|
|
622
745
|
}
|
|
623
746
|
|
|
624
747
|
/**
|
|
@@ -635,8 +758,12 @@ function planClaudeRegistration({ existingOutput, wantedUrl }) {
|
|
|
635
758
|
* `deps` is for tests only: `get` returns the `claude mcp get niro` output or null, `run` runs a
|
|
636
759
|
* client command; both default to the real thing.
|
|
637
760
|
*/
|
|
638
|
-
async function setupClaudeCode(apiKey, mcpServerUrl, deps = {}) {
|
|
639
|
-
|
|
761
|
+
async function setupClaudeCode(apiKey, mcpServerUrl, deps = {}, mode = "cloud") {
|
|
762
|
+
// A release without the cloud (src/lib/release.js) sets Claude Code up for the engine on this machine: the bridge
|
|
763
|
+
// in local mode and the hooks that read niro-local's own list of indexed folders, not the cloud hooks that look
|
|
764
|
+
// for a `.niro-project` file and so never fire in a Niro Local folder (Niraj, 2026-11-06, Q5).
|
|
765
|
+
if (!require("../lib/release").cloud) mode = "local";
|
|
766
|
+
console.log(`\n Configuring Claude Code${mode === "local" ? " for niro-local, the engine on this machine" : ""}...\n`);
|
|
640
767
|
const run = deps.run || runClientCommand;
|
|
641
768
|
const get = deps.get || (() => {
|
|
642
769
|
try {
|
|
@@ -651,16 +778,25 @@ async function setupClaudeCode(apiKey, mcpServerUrl, deps = {}) {
|
|
|
651
778
|
"--scope", "user",
|
|
652
779
|
"niro",
|
|
653
780
|
];
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
781
|
+
if (mode === "local") {
|
|
782
|
+
// The same bridge, told to answer everything from the engine on this machine: initialize and the
|
|
783
|
+
// tool list as well as tool calls (src/mcp/server.js, localSend). No server URL and no key: nothing
|
|
784
|
+
// is asked of the cloud to answer, and the licence comes from the `niro login` session.
|
|
785
|
+
cmd.push("-e", "NIRO_MCP_MODE=local");
|
|
786
|
+
} else {
|
|
787
|
+
// Only embed NIRO_API_KEY when there is no cli-token (legacy/key flow). With a `niro login`
|
|
788
|
+
// session the server reads the cli-token from ~/.niro/credentials.json at runtime.
|
|
789
|
+
if (apiKey) cmd.push("-e", `NIRO_API_KEY=${apiKey}`);
|
|
790
|
+
// The bridge (src/mcp/server.js) reads NIRO_MCP_SERVER_URL (ai-assistant), not NIRO_API_URL.
|
|
791
|
+
cmd.push("-e", `NIRO_MCP_SERVER_URL=${mcpServerUrl}`);
|
|
792
|
+
}
|
|
659
793
|
cmd.push("--", "niro", "mcp", "serve");
|
|
660
794
|
|
|
661
|
-
const plan = planClaudeRegistration({ existingOutput: get(), wantedUrl: mcpServerUrl });
|
|
795
|
+
const plan = planClaudeRegistration({ existingOutput: get(), wantedUrl: mcpServerUrl, mode });
|
|
662
796
|
if (plan === "keep") {
|
|
663
|
-
console.log(
|
|
797
|
+
console.log(mode === "local"
|
|
798
|
+
? " MCP server already registered in Claude Code for niro-local. Keeping it.\n"
|
|
799
|
+
: ` MCP server already registered in Claude Code at ${mcpServerUrl}. Keeping it.\n`);
|
|
664
800
|
} else {
|
|
665
801
|
if (plan === "replace") {
|
|
666
802
|
try {
|
|
@@ -681,12 +817,37 @@ async function setupClaudeCode(apiKey, mcpServerUrl, deps = {}) {
|
|
|
681
817
|
}
|
|
682
818
|
}
|
|
683
819
|
|
|
820
|
+
if (mode === "local") {
|
|
821
|
+
addClaudeCodePostCompactHook(localMode.LOCAL_POST_COMPACT_COMMAND);
|
|
822
|
+
addClaudeCodeUserPromptHook(localMode.LOCAL_USER_PROMPT_COMMAND);
|
|
823
|
+
// Temporary projects are a cloud idea: niro-local follows the folder whatever branch it is on.
|
|
824
|
+
removeClaudeCodeTempProjectHook();
|
|
825
|
+
addClaudeCodeExploreHook(localMode.localHook(NIRO_EXPLORE_COMMAND));
|
|
826
|
+
shellSearchHook.addClaudeCodeShellSearchHook(localMode.localHook(shellSearchHook.NIRO_SHELL_SEARCH_COMMAND));
|
|
827
|
+
addClaudeCodeImpactHook(localMode.localHook(NIRO_IMPACT_COMMAND));
|
|
828
|
+
removeStaleNiroStopHook();
|
|
829
|
+
addClaudeCodeLineCheckHook();
|
|
830
|
+
addClaudeCodeMemory(localMode.LOCAL_CLAUDE_MD_SECTION);
|
|
831
|
+
// The two skills: local mode's own in a cloud release. With the cloud off, the release's own (src/mcp/localSkill.js):
|
|
832
|
+
// the tool skill naming the engine's tools, and the `niro-local` command skill, whose commands (`niro init`,
|
|
833
|
+
// `niro build`, `niro plan`) are the ones that release has (Niraj, 2026-11-06, Q5: our hooks, her skill text).
|
|
834
|
+
if (require("../lib/release").cloud) {
|
|
835
|
+
addClaudeCodeSkill(localMode.LOCAL_SKILL);
|
|
836
|
+
addClaudeCodeCliSkill(localMode.LOCAL_CLI_SKILL);
|
|
837
|
+
} else {
|
|
838
|
+
addClaudeCodeSkill();
|
|
839
|
+
addClaudeCodeCliSkill();
|
|
840
|
+
}
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
684
843
|
addClaudeCodePostCompactHook();
|
|
685
844
|
addClaudeCodeUserPromptHook();
|
|
686
845
|
addClaudeCodePreToolUseHook();
|
|
687
846
|
addClaudeCodeExploreHook();
|
|
847
|
+
shellSearchHook.addClaudeCodeShellSearchHook();
|
|
688
848
|
addClaudeCodeImpactHook();
|
|
689
849
|
removeStaleNiroStopHook();
|
|
850
|
+
addClaudeCodeLineCheckHook();
|
|
690
851
|
addClaudeCodeMemory();
|
|
691
852
|
addClaudeCodeSkill();
|
|
692
853
|
addClaudeCodeCliSkill();
|
|
@@ -694,6 +855,31 @@ async function setupClaudeCode(apiKey, mcpServerUrl, deps = {}) {
|
|
|
694
855
|
return true;
|
|
695
856
|
}
|
|
696
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Take niro's temporary-project hook out of ~/.claude/settings.json (local mode, where it does not
|
|
860
|
+
* apply). Only entries isNiroPreToolUseHook recognises are touched; a missing or unreadable file is
|
|
861
|
+
* left as it is.
|
|
862
|
+
*/
|
|
863
|
+
function removeClaudeCodeTempProjectHook() {
|
|
864
|
+
const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
865
|
+
let settings;
|
|
866
|
+
try {
|
|
867
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
868
|
+
} catch {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const hooks = settings && settings.hooks && settings.hooks.PreToolUse;
|
|
872
|
+
if (!Array.isArray(hooks) || !hooks.some(isNiroPreToolUseHook)) return;
|
|
873
|
+
settings.hooks.PreToolUse = hooks.filter((h) => !isNiroPreToolUseHook(h));
|
|
874
|
+
if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
|
|
875
|
+
try {
|
|
876
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
877
|
+
console.log(" PreToolUse temporary project hook removed (it does not apply to niro-local).\n");
|
|
878
|
+
} catch (err) {
|
|
879
|
+
console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
697
883
|
/**
|
|
698
884
|
* Adds a PostCompact hook to ~/.claude/settings.json so Claude is reminded
|
|
699
885
|
* about Niro tools after context compaction (the point where tool awareness
|
|
@@ -742,16 +928,15 @@ const NIRO_POST_COMPACT_COMMAND =
|
|
|
742
928
|
+ "Keep that list and verify those flows after the edit; the graph lags your edit, so do not re-query it to verify. "
|
|
743
929
|
+ "A log line or comment needs no map.'";
|
|
744
930
|
|
|
745
|
-
function addClaudeCodePostCompactHook() {
|
|
931
|
+
function addClaudeCodePostCompactHook(hookCommand = NIRO_POST_COMPACT_COMMAND) {
|
|
746
932
|
// Guard the invariant that keeps upgrade-in-place working: if the reminder is ever reworded so it
|
|
747
933
|
// no longer starts with the generated prefix, every install would append a fresh entry instead of
|
|
748
934
|
// replacing the old one. Fail loudly at the boundary rather than quietly stacking hooks.
|
|
749
|
-
if (!
|
|
750
|
-
throw new Error("
|
|
935
|
+
if (!hookCommand.startsWith(NIRO_POST_COMPACT_PREFIX)) {
|
|
936
|
+
throw new Error("the PostCompact command must start with NIRO_POST_COMPACT_PREFIX, or upgrade-in-place breaks");
|
|
751
937
|
}
|
|
752
938
|
|
|
753
939
|
const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
754
|
-
const hookCommand = NIRO_POST_COMPACT_COMMAND;
|
|
755
940
|
|
|
756
941
|
let settings = {};
|
|
757
942
|
try {
|
|
@@ -801,9 +986,8 @@ function addClaudeCodePostCompactHook() {
|
|
|
801
986
|
* Appends a Niro reminder section to ~/.claude/CLAUDE.md so Claude is aware
|
|
802
987
|
* of Niro tools across all projects and sessions.
|
|
803
988
|
*/
|
|
804
|
-
function addClaudeCodeMemory() {
|
|
989
|
+
function addClaudeCodeMemory(section = forRelease(NIRO_CLAUDE_MD_SECTION)) {
|
|
805
990
|
const claudeMdPath = path.join(process.env.HOME || "", ".claude", "CLAUDE.md");
|
|
806
|
-
const section = NIRO_CLAUDE_MD_SECTION;
|
|
807
991
|
|
|
808
992
|
let existing = "";
|
|
809
993
|
try {
|
|
@@ -900,16 +1084,15 @@ function isNiroGeneratedUserPromptHook(h) {
|
|
|
900
1084
|
);
|
|
901
1085
|
}
|
|
902
1086
|
|
|
903
|
-
function addClaudeCodeUserPromptHook() {
|
|
1087
|
+
function addClaudeCodeUserPromptHook(hookCommand = NIRO_USER_PROMPT_COMMAND) {
|
|
904
1088
|
// Guard the invariant that keeps upgrade-in-place working: if the reminder is reworded so it no longer
|
|
905
1089
|
// carries a known generated marker, every install would append a fresh entry instead of replacing the
|
|
906
1090
|
// old one. Fail loudly at the boundary rather than quietly stacking hooks in the user's settings.
|
|
907
|
-
if (!NIRO_USER_PROMPT_GENERATED_MARKERS.some((m) =>
|
|
908
|
-
throw new Error("
|
|
1091
|
+
if (!NIRO_USER_PROMPT_GENERATED_MARKERS.some((m) => hookCommand.includes(m))) {
|
|
1092
|
+
throw new Error("the UserPromptSubmit command must contain a known generated marker, or upgrade-in-place breaks");
|
|
909
1093
|
}
|
|
910
1094
|
const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
911
|
-
|
|
912
|
-
|
|
1095
|
+
|
|
913
1096
|
let settings = {};
|
|
914
1097
|
try {
|
|
915
1098
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -991,6 +1174,51 @@ const NIRO_PRETOOL_COMMAND = [
|
|
|
991
1174
|
`printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"Niro temporary project check: you are editing a Niro-indexed repo on its CURRENT branch. The temporary project answer is per branch, so a check you ran on a different branch (e.g. before switching) does NOT count here. Call should_create_temp_project(git_url, branch=current branch, project_id from its .niro-project) for THIS repo now and run niro new-temp-project if it returns needs_temp_project. Shown once per repo per branch per session."}}'`,
|
|
992
1175
|
].join('; ');
|
|
993
1176
|
|
|
1177
|
+
/** The subcommand the line-check hook runs, which is also how the installer recognises its own. */
|
|
1178
|
+
const NIRO_LINE_CHECK_SUBCOMMAND = "mcp line-check";
|
|
1179
|
+
/**
|
|
1180
|
+
* Guarded, because this one fires on every stop in every session, Niro or not. A person who removed the
|
|
1181
|
+
* package without `niro mcp uninstall` would otherwise see an error at the end of every turn.
|
|
1182
|
+
*/
|
|
1183
|
+
const NIRO_LINE_CHECK_COMMAND = `command -v niro >/dev/null 2>&1 && niro ${NIRO_LINE_CHECK_SUBCOMMAND} || true`;
|
|
1184
|
+
/** Seconds. Claude Code's default for a command hook is 600, far too long to hold up a stop. */
|
|
1185
|
+
const NIRO_LINE_CHECK_TIMEOUT = 5;
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Adds the Stop hook that asks the assistant, once, for an end-of-task line it forgot
|
|
1189
|
+
* (src/mcp/lineCheck.js). Same file, same shape, and the same care as the other hooks: an unreadable
|
|
1190
|
+
* settings file is left alone, and anything not recognised by isNiroLineCheckHook is never touched.
|
|
1191
|
+
*/
|
|
1192
|
+
function addClaudeCodeLineCheckHook() {
|
|
1193
|
+
const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
1194
|
+
let settings = {};
|
|
1195
|
+
try {
|
|
1196
|
+
if (fs.existsSync(settingsPath)) settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
console.warn(` Could not read ~/.claude/settings.json, so the end-of-task check was not added: ${err.message}`);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const entry = {
|
|
1202
|
+
matcher: "",
|
|
1203
|
+
hooks: [{ type: "command", command: NIRO_LINE_CHECK_COMMAND, timeout: NIRO_LINE_CHECK_TIMEOUT }],
|
|
1204
|
+
};
|
|
1205
|
+
if (!settings.hooks) settings.hooks = {};
|
|
1206
|
+
const stop = Array.isArray(settings.hooks.Stop) ? settings.hooks.Stop : [];
|
|
1207
|
+
const current = stop.filter(isNiroLineCheckHook);
|
|
1208
|
+
if (current.length === 1 && JSON.stringify(current[0]) === JSON.stringify(entry)) {
|
|
1209
|
+
console.log(" End-of-task check already up to date. Skipping.\n");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
settings.hooks.Stop = [...stop.filter((h) => !isNiroLineCheckHook(h)), entry];
|
|
1213
|
+
try {
|
|
1214
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
1215
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1216
|
+
console.log(" End-of-task check added in ~/.claude/settings.json: it asks for the Niro line when it was forgotten.\n");
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
994
1222
|
/**
|
|
995
1223
|
* Removes a Stop hook left behind by an earlier version of this installer.
|
|
996
1224
|
*
|
|
@@ -1136,7 +1364,7 @@ const NIRO_EXPLORE_COMMAND = [
|
|
|
1136
1364
|
* upgrades a stale niro entry in place, never touches a foreign PreToolUse hook on the same matcher
|
|
1137
1365
|
* (matched by the niro-explore- marker via isNiroExploreHook).
|
|
1138
1366
|
*/
|
|
1139
|
-
function addClaudeCodeExploreHook() {
|
|
1367
|
+
function addClaudeCodeExploreHook(hookCommand = NIRO_EXPLORE_COMMAND) {
|
|
1140
1368
|
const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
1141
1369
|
let settings = {};
|
|
1142
1370
|
try {
|
|
@@ -1148,9 +1376,9 @@ function addClaudeCodeExploreHook() {
|
|
|
1148
1376
|
if (!settings.hooks) settings.hooks = {};
|
|
1149
1377
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
1150
1378
|
|
|
1151
|
-
const entry = { matcher: "Grep|Glob|Read", hooks: [{ type: "command", command:
|
|
1379
|
+
const entry = { matcher: "Grep|Glob|Read", hooks: [{ type: "command", command: hookCommand }] };
|
|
1152
1380
|
const alreadyCurrent = settings.hooks.PreToolUse.some(
|
|
1153
|
-
(h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command ===
|
|
1381
|
+
(h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === hookCommand),
|
|
1154
1382
|
);
|
|
1155
1383
|
if (alreadyCurrent) {
|
|
1156
1384
|
console.log(" PreToolUse explore-first hook already up to date. Skipping.\n");
|
|
@@ -1293,7 +1521,7 @@ const NIRO_IMPACT_COMMAND = [
|
|
|
1293
1521
|
* stale niro entry in place, never touches a foreign PreToolUse hook on the same matcher nor niro's own
|
|
1294
1522
|
* temp-project hook (matched by the niro-impact- token via isNiroImpactHook).
|
|
1295
1523
|
*/
|
|
1296
|
-
function addClaudeCodeImpactHook() {
|
|
1524
|
+
function addClaudeCodeImpactHook(hookCommand = NIRO_IMPACT_COMMAND) {
|
|
1297
1525
|
const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
1298
1526
|
let settings = {};
|
|
1299
1527
|
try {
|
|
@@ -1305,9 +1533,9 @@ function addClaudeCodeImpactHook() {
|
|
|
1305
1533
|
if (!settings.hooks) settings.hooks = {};
|
|
1306
1534
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
1307
1535
|
|
|
1308
|
-
const entry = { matcher: "Edit|Write", hooks: [{ type: "command", command:
|
|
1536
|
+
const entry = { matcher: "Edit|Write", hooks: [{ type: "command", command: hookCommand }] };
|
|
1309
1537
|
const alreadyCurrent = settings.hooks.PreToolUse.some(
|
|
1310
|
-
(h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command ===
|
|
1538
|
+
(h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === hookCommand),
|
|
1311
1539
|
);
|
|
1312
1540
|
if (alreadyCurrent) {
|
|
1313
1541
|
console.log(" PreToolUse impact-gate hook already up to date. Skipping.\n");
|
|
@@ -1570,6 +1798,8 @@ You manage a user's Niro projects by running the \`niro\` CLI via the shell (Bas
|
|
|
1570
1798
|
its \`--json\` output. You are a PARAM-COLLECTOR, not a decision engine: gather the inputs you
|
|
1571
1799
|
need in conversation, then run ONE non-interactive \`niro\` command, parse the JSON, and report
|
|
1572
1800
|
back in plain English. Do not invent business logic — the CLI owns it.
|
|
1801
|
+
For a Niro error message or a how-to question, run \`niro guide --search <the message>\` first and
|
|
1802
|
+
answer from the page it names (\`niro guide <page>\` prints it; the pages ship with niro, offline).
|
|
1573
1803
|
|
|
1574
1804
|
## Golden rules (apply to EVERY task)
|
|
1575
1805
|
|
|
@@ -1737,10 +1967,10 @@ state, and the next sensible step. Never dump raw secrets or env values.
|
|
|
1737
1967
|
* Writes the `niro` skill to ~/.claude/skills/niro-skill/SKILL.md.
|
|
1738
1968
|
* This file is niro-generated; it is refreshed when the bundled content changes.
|
|
1739
1969
|
*/
|
|
1740
|
-
function addClaudeCodeSkill() {
|
|
1970
|
+
function addClaudeCodeSkill(content = toolSkill()) {
|
|
1741
1971
|
const skillPath = path.join(process.env.HOME || "", ".claude", "skills", "niro-skill", "SKILL.md");
|
|
1742
1972
|
try {
|
|
1743
|
-
const result = writeManagedSkill(skillPath,
|
|
1973
|
+
const result = writeManagedSkill(skillPath, content, "niro skill");
|
|
1744
1974
|
if (result === "added" || result === "updated") {
|
|
1745
1975
|
}
|
|
1746
1976
|
console.log("");
|
|
@@ -1749,15 +1979,47 @@ function addClaudeCodeSkill() {
|
|
|
1749
1979
|
}
|
|
1750
1980
|
}
|
|
1751
1981
|
|
|
1982
|
+
/**
|
|
1983
|
+
* Which CLI skill this release installs: the cloud `niro-cli` playbook, or, with the cloud off
|
|
1984
|
+
* (src/lib/release.js), the `niro-local` one, whose commands are the ones this release has.
|
|
1985
|
+
*/
|
|
1986
|
+
function cliSkill(cloudLabel) {
|
|
1987
|
+
if (require("../lib/release").cloud) return { dir: "niro-cli", body: NIRO_CLI_SKILL, label: cloudLabel };
|
|
1988
|
+
return { dir: "niro-local", body: require("./localSkill").NIRO_LOCAL_SKILL, label: cloudLabel.replace("niro-cli", "niro-local") };
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/** The tool-usage skill: the cloud's, or with the cloud off one naming only the local engine's tools. */
|
|
1992
|
+
function toolSkill() {
|
|
1993
|
+
return require("../lib/release").cloud ? NIRO_SKILL : require("./localSkill").NIRO_SKILL_LOCAL;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/** Take out a `niro-cli` skill an earlier install wrote, so it does not point at hidden commands. */
|
|
1997
|
+
function dropCloudCliSkill(skillsDir) {
|
|
1998
|
+
const file = path.join(skillsDir, "niro-cli", "SKILL.md");
|
|
1999
|
+
try {
|
|
2000
|
+
if (!fs.existsSync(file)) return;
|
|
2001
|
+
fs.unlinkSync(file);
|
|
2002
|
+
try { fs.rmdirSync(path.dirname(file)); } catch { /* not empty: something else lives there */ }
|
|
2003
|
+
console.log(` niro-cli skill removed (this release uses niro-local): ${file}`);
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
console.warn(` Could not remove ${file}: ${err.message}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1752
2009
|
/**
|
|
1753
2010
|
* Writes the `niro-cli` skill to ~/.claude/skills/niro-cli/SKILL.md.
|
|
1754
2011
|
* This file is niro-generated; it is refreshed when the bundled content changes.
|
|
1755
2012
|
* Mirrors addClaudeCodeSkill.
|
|
1756
2013
|
*/
|
|
1757
|
-
function addClaudeCodeCliSkill() {
|
|
1758
|
-
|
|
2014
|
+
function addClaudeCodeCliSkill(content) {
|
|
2015
|
+
// No content: this release's own CLI skill (the cloud `niro-cli` one, or `niro-local` with the cloud off). Content:
|
|
2016
|
+
// local mode in a cloud release, which writes its own text to the `niro-cli` skill file.
|
|
2017
|
+
const { dir, body, label } =
|
|
2018
|
+
content === undefined ? cliSkill("niro-cli skill") : { dir: "niro-cli", body: content, label: "niro-cli skill" };
|
|
2019
|
+
const skillPath = path.join(process.env.HOME || "", ".claude", "skills", dir, "SKILL.md");
|
|
2020
|
+
if (dir !== "niro-cli") dropCloudCliSkill(path.join(process.env.HOME || "", ".claude", "skills"));
|
|
1759
2021
|
try {
|
|
1760
|
-
const result = writeManagedSkill(skillPath,
|
|
2022
|
+
const result = writeManagedSkill(skillPath, body, label);
|
|
1761
2023
|
if (result === "added" || result === "updated") {
|
|
1762
2024
|
}
|
|
1763
2025
|
console.log("");
|
|
@@ -1823,24 +2085,59 @@ Routing (most specific wins):
|
|
|
1823
2085
|
- data & events: get_data_resource_usage | get_event_producers_and_handlers
|
|
1824
2086
|
- search / status: code_grep | get_index_status
|
|
1825
2087
|
|
|
2088
|
+
${NIRO_GUIDE_SENTENCE}
|
|
2089
|
+
|
|
1826
2090
|
Onboarding / indexing a repo is done in the terminal with the niro CLI (niro init), not
|
|
1827
2091
|
via MCP tools.
|
|
1828
2092
|
`;
|
|
1829
2093
|
|
|
2094
|
+
/** How every version of NIRO_CURSOR_RULE has begun and ended (all six in git history, 2026-11-06). */
|
|
2095
|
+
const CURSOR_RULE_START = "---\ndescription: Niro code-intelligence";
|
|
2096
|
+
const CURSOR_RULE_END = "\nvia MCP tools.\n";
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* A Cursor rule file with Niro's part replaced by `fresh`, and every line the user added before or after it kept.
|
|
2100
|
+
* Niro's part is the span from its front matter to its last line ("via MCP tools."), which every version written so
|
|
2101
|
+
* far shares. Returns {content, action}: "unchanged" when it already says `fresh`, "refreshed" when Niro's part was
|
|
2102
|
+
* replaced, and "foreign" for a file Niro did not write (no such span), which is left alone. A line typed INSIDE
|
|
2103
|
+
* Niro's part goes with it, as it does for the markdown sections (upsertNiroMarkerSection).
|
|
2104
|
+
*/
|
|
2105
|
+
function refreshCursorRule(existing, fresh) {
|
|
2106
|
+
const start = existing.indexOf(CURSOR_RULE_START);
|
|
2107
|
+
const endAt = start === -1 ? -1 : existing.indexOf(CURSOR_RULE_END, start);
|
|
2108
|
+
const ending = endAt === -1 && start !== -1 && existing.endsWith(CURSOR_RULE_END.trimEnd()) ? existing.length : -1;
|
|
2109
|
+
if (start === -1 || (endAt === -1 && ending === -1)) return { content: existing, action: "foreign" };
|
|
2110
|
+
const end = endAt !== -1 ? endAt + CURSOR_RULE_END.length : ending;
|
|
2111
|
+
const content = existing.slice(0, start) + fresh + existing.slice(end);
|
|
2112
|
+
return { content, action: content === existing ? "unchanged" : "refreshed" };
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1830
2115
|
/**
|
|
1831
2116
|
* Writes the Niro Cursor rule to <cwd>/.cursor/rules/niro.mdc. Cursor rules are
|
|
1832
2117
|
* PROJECT-scoped (unlike the global ~/.cursor/mcp.json), so this lands in the repo the
|
|
1833
|
-
* wizard is run from.
|
|
2118
|
+
* wizard is run from. A rule an earlier install wrote is refreshed, keeping the user's own lines
|
|
2119
|
+
* (refreshCursorRule): until 2026-11-06 an existing file was skipped, so a rule written by a cloud
|
|
2120
|
+
* install kept naming resolve_fqn and four cloud-only tools after an upgrade to this release
|
|
2121
|
+
* (the checker's note 3 on fix/cli-small).
|
|
1834
2122
|
*/
|
|
1835
2123
|
function ensureCursorRule() {
|
|
1836
2124
|
const rulePath = path.join(process.cwd(), ".cursor", "rules", "niro.mdc");
|
|
1837
|
-
|
|
1838
|
-
console.log(" Cursor rule already present. Skipping (.cursor/rules/niro.mdc).\n");
|
|
1839
|
-
return;
|
|
1840
|
-
}
|
|
2125
|
+
const fresh = forRelease(NIRO_CURSOR_RULE);
|
|
1841
2126
|
try {
|
|
2127
|
+
if (fs.existsSync(rulePath)) {
|
|
2128
|
+
const { content, action } = refreshCursorRule(fs.readFileSync(rulePath, "utf-8"), fresh);
|
|
2129
|
+
if (action === "foreign") {
|
|
2130
|
+
console.log(" A Cursor rule Niro did not write is at .cursor/rules/niro.mdc. Leaving it as it is.\n");
|
|
2131
|
+
} else if (action === "unchanged") {
|
|
2132
|
+
console.log(" Cursor rule already up to date. Skipping (.cursor/rules/niro.mdc).\n");
|
|
2133
|
+
} else {
|
|
2134
|
+
fs.writeFileSync(rulePath, content);
|
|
2135
|
+
console.log(` Cursor rule refreshed, your own lines kept: ${rulePath}\n`);
|
|
2136
|
+
}
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
1842
2139
|
fs.mkdirSync(path.dirname(rulePath), { recursive: true });
|
|
1843
|
-
fs.writeFileSync(rulePath,
|
|
2140
|
+
fs.writeFileSync(rulePath, fresh);
|
|
1844
2141
|
console.log(` Cursor rule added: ${rulePath}`);
|
|
1845
2142
|
console.log(" • Project-scoped. For other repos, copy it or re-run setup there.\n");
|
|
1846
2143
|
} catch (err) {
|
|
@@ -1859,9 +2156,8 @@ async function setupCodex(apiKey, mcpServerUrl) {
|
|
|
1859
2156
|
"codex", "mcp", "add",
|
|
1860
2157
|
"niro",
|
|
1861
2158
|
];
|
|
1862
|
-
if (apiKey) cmd.push("--env", `NIRO_API_KEY=${apiKey}`);
|
|
1863
2159
|
// The bridge reads NIRO_MCP_SERVER_URL (ai-assistant), not NIRO_API_URL.
|
|
1864
|
-
cmd.push("--env",
|
|
2160
|
+
for (const [key, value] of Object.entries(bridgeEnv(apiKey, mcpServerUrl))) cmd.push("--env", `${key}=${value}`);
|
|
1865
2161
|
cmd.push("--", "niro", "mcp", "serve");
|
|
1866
2162
|
|
|
1867
2163
|
try {
|
|
@@ -2108,7 +2404,7 @@ function locateCodexNiroTable(cfgPath) {
|
|
|
2108
2404
|
*/
|
|
2109
2405
|
function addCodexMemory() {
|
|
2110
2406
|
const agentsPath = path.join(process.env.HOME || "", ".codex", "AGENTS.md");
|
|
2111
|
-
const section = NIRO_CODEX_MD_SECTION;
|
|
2407
|
+
const section = forRelease(NIRO_CODEX_MD_SECTION);
|
|
2112
2408
|
|
|
2113
2409
|
let existing = "";
|
|
2114
2410
|
try {
|
|
@@ -2146,7 +2442,7 @@ function addCodexMemory() {
|
|
|
2146
2442
|
function addAgentsSkill() {
|
|
2147
2443
|
const skillPath = path.join(process.env.HOME || "", ".agents", "skills", "niro-skill", "SKILL.md");
|
|
2148
2444
|
try {
|
|
2149
|
-
const result = writeManagedSkill(skillPath,
|
|
2445
|
+
const result = writeManagedSkill(skillPath, toolSkill(), "Shared niro-skill");
|
|
2150
2446
|
if (result === "added" || result === "updated") {
|
|
2151
2447
|
console.log(" • Read by Codex and Windsurf (cross-agent skills location).");
|
|
2152
2448
|
}
|
|
@@ -2162,9 +2458,11 @@ function addAgentsSkill() {
|
|
|
2162
2458
|
* playbook. Reuses NIRO_CLI_SKILL. This file is niro-generated; refreshed on change.
|
|
2163
2459
|
*/
|
|
2164
2460
|
function addAgentsCliSkill() {
|
|
2165
|
-
const
|
|
2461
|
+
const { dir, body, label } = cliSkill("Shared niro-cli skill");
|
|
2462
|
+
const skillPath = path.join(process.env.HOME || "", ".agents", "skills", dir, "SKILL.md");
|
|
2463
|
+
if (dir !== "niro-cli") dropCloudCliSkill(path.join(process.env.HOME || "", ".agents", "skills"));
|
|
2166
2464
|
try {
|
|
2167
|
-
const result = writeManagedSkill(skillPath,
|
|
2465
|
+
const result = writeManagedSkill(skillPath, body, label);
|
|
2168
2466
|
if (result === "added" || result === "updated") {
|
|
2169
2467
|
console.log(" • Read by Codex and Windsurf (cross-agent skills location).");
|
|
2170
2468
|
}
|
|
@@ -2181,7 +2479,7 @@ function addAgentsCliSkill() {
|
|
|
2181
2479
|
*/
|
|
2182
2480
|
function addWindsurfGlobalRule() {
|
|
2183
2481
|
const rulePath = path.join(process.env.HOME || "", ".codeium", "windsurf", "memories", "global_rules.md");
|
|
2184
|
-
const section = NIRO_WINDSURF_MD_SECTION;
|
|
2482
|
+
const section = forRelease(NIRO_WINDSURF_MD_SECTION);
|
|
2185
2483
|
|
|
2186
2484
|
let existing = "";
|
|
2187
2485
|
try {
|
|
@@ -2210,7 +2508,7 @@ function addWindsurfGlobalRule() {
|
|
|
2210
2508
|
* the generic version of addCodexMemory/addWindsurfGlobalRule for clients whose "memory"
|
|
2211
2509
|
* file has no client-specific extras (skills, hooks) — just the terse reminder.
|
|
2212
2510
|
*/
|
|
2213
|
-
function appendMarkerSectionToFile(absPath, label, section = NIRO_GENERIC_MD_SECTION) {
|
|
2511
|
+
function appendMarkerSectionToFile(absPath, label, section = forRelease(NIRO_GENERIC_MD_SECTION)) {
|
|
2214
2512
|
let existing = "";
|
|
2215
2513
|
try {
|
|
2216
2514
|
if (fs.existsSync(absPath)) existing = fs.readFileSync(absPath, "utf-8");
|
|
@@ -2275,6 +2573,8 @@ The graph updates only after your edit is synced and rebuilt, so do not re-query
|
|
|
2275
2573
|
change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
|
|
2276
2574
|
in one sentence.
|
|
2277
2575
|
|
|
2576
|
+
${NIRO_GUIDE_SENTENCE}
|
|
2577
|
+
|
|
2278
2578
|
If the niro tools are not present in this session, or the repo is not indexed (no project
|
|
2279
2579
|
resolves), fall back to native tools silently — do not mention Niro's absence.
|
|
2280
2580
|
`;
|
|
@@ -2288,7 +2588,7 @@ function addClineRule() {
|
|
|
2288
2588
|
const home = userHome();
|
|
2289
2589
|
const rulePath = path.join(home, "Documents", "Cline", "Rules", "niro.md");
|
|
2290
2590
|
try {
|
|
2291
|
-
const result = writeManagedSkill(rulePath, NIRO_CLINE_RULE, "Cline rule");
|
|
2591
|
+
const result = writeManagedSkill(rulePath, forRelease(NIRO_CLINE_RULE), "Cline rule");
|
|
2292
2592
|
if (result === "added" || result === "updated") {
|
|
2293
2593
|
console.log(" • Global: applies across all your repos in Cline.");
|
|
2294
2594
|
}
|
|
@@ -2331,6 +2631,8 @@ The graph updates only after your edit is synced and rebuilt, so do not re-query
|
|
|
2331
2631
|
change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
|
|
2332
2632
|
in one sentence.
|
|
2333
2633
|
|
|
2634
|
+
${NIRO_GUIDE_SENTENCE}
|
|
2635
|
+
|
|
2334
2636
|
If the niro tools are not present in this session, or the repo is not indexed (no project
|
|
2335
2637
|
resolves), fall back to native tools silently — do not mention Niro's absence.
|
|
2336
2638
|
`;
|
|
@@ -2338,7 +2640,7 @@ resolves), fall back to native tools silently — do not mention Niro's absence.
|
|
|
2338
2640
|
function addKiroSteering() {
|
|
2339
2641
|
const rulePath = path.join(userHome(), ".kiro", "steering", "niro.md");
|
|
2340
2642
|
try {
|
|
2341
|
-
const result = writeManagedSkill(rulePath, NIRO_KIRO_STEERING, "Kiro steering file");
|
|
2643
|
+
const result = writeManagedSkill(rulePath, forRelease(NIRO_KIRO_STEERING), "Kiro steering file");
|
|
2342
2644
|
if (result === "added" || result === "updated") {
|
|
2343
2645
|
console.log(" • Global: applies across all your repos in Kiro.");
|
|
2344
2646
|
}
|
|
@@ -2356,8 +2658,7 @@ async function setupGeminiCli(apiKey, mcpServerUrl) {
|
|
|
2356
2658
|
console.log("\n Configuring Gemini CLI...\n");
|
|
2357
2659
|
|
|
2358
2660
|
const cmd = ["gemini", "mcp", "add", "-s", "user"];
|
|
2359
|
-
|
|
2360
|
-
cmd.push("-e", `NIRO_MCP_SERVER_URL=${mcpServerUrl}`);
|
|
2661
|
+
for (const [key, value] of Object.entries(bridgeEnv(apiKey, mcpServerUrl))) cmd.push("-e", `${key}=${value}`);
|
|
2361
2662
|
cmd.push("niro", "niro", "mcp", "serve");
|
|
2362
2663
|
|
|
2363
2664
|
try {
|
|
@@ -2382,8 +2683,7 @@ async function setupCopilotCli(apiKey, mcpServerUrl) {
|
|
|
2382
2683
|
console.log("\n Configuring GitHub Copilot CLI...\n");
|
|
2383
2684
|
|
|
2384
2685
|
const cmd = ["copilot", "mcp", "add", "niro"];
|
|
2385
|
-
|
|
2386
|
-
cmd.push("--env", `NIRO_MCP_SERVER_URL=${mcpServerUrl}`);
|
|
2686
|
+
for (const [key, value] of Object.entries(bridgeEnv(apiKey, mcpServerUrl))) cmd.push("--env", `${key}=${value}`);
|
|
2387
2687
|
cmd.push("--", "niro", "mcp", "serve");
|
|
2388
2688
|
|
|
2389
2689
|
try {
|
|
@@ -2410,8 +2710,7 @@ async function setupFactory(apiKey, mcpServerUrl) {
|
|
|
2410
2710
|
console.log("\n Configuring Factory (droid)...\n");
|
|
2411
2711
|
|
|
2412
2712
|
const cmd = ["droid", "mcp", "add", "niro", '"niro mcp serve"'];
|
|
2413
|
-
|
|
2414
|
-
cmd.push("--env", `NIRO_MCP_SERVER_URL=${mcpServerUrl}`);
|
|
2713
|
+
for (const [key, value] of Object.entries(bridgeEnv(apiKey, mcpServerUrl))) cmd.push("--env", `${key}=${value}`);
|
|
2415
2714
|
|
|
2416
2715
|
try {
|
|
2417
2716
|
execSync(cmd.join(" "), { stdio: "inherit" });
|
|
@@ -2443,8 +2742,9 @@ function buildGooseNiroBlock(apiKey, mcpServerUrl) {
|
|
|
2443
2742
|
// Callers (setupGoose, dispatched only from main()) already ran both values through
|
|
2444
2743
|
// hasShellMetacharacters, which also rejects the quotes/backslashes that would break this
|
|
2445
2744
|
// YAML string — safe to embed directly.
|
|
2446
|
-
const
|
|
2447
|
-
|
|
2745
|
+
const { NIRO_API_KEY: key, ...rest } = bridgeEnv(apiKey, mcpServerUrl);
|
|
2746
|
+
const envs = [" envs:", ...Object.entries(rest).map(([name, value]) => ` ${name}: "${value}"`)];
|
|
2747
|
+
if (key) envs.push(` NIRO_API_KEY: "${key}"`);
|
|
2448
2748
|
return [
|
|
2449
2749
|
" niro:",
|
|
2450
2750
|
" name: niro",
|
|
@@ -2572,9 +2872,7 @@ async function setupJsonConfig(clientId, apiKey, mcpServerUrl) {
|
|
|
2572
2872
|
// The bridge reads NIRO_MCP_SERVER_URL (ai-assistant) — always include it.
|
|
2573
2873
|
// Omit NIRO_API_KEY when a cli-token is available; the server reads it from
|
|
2574
2874
|
// ~/.niro/credentials.json at runtime.
|
|
2575
|
-
env: apiKey
|
|
2576
|
-
? { NIRO_API_KEY: apiKey, NIRO_MCP_SERVER_URL: mcpServerUrl }
|
|
2577
|
-
: { NIRO_MCP_SERVER_URL: mcpServerUrl },
|
|
2875
|
+
env: bridgeEnv(apiKey, mcpServerUrl),
|
|
2578
2876
|
};
|
|
2579
2877
|
// Cursor honours explicit stdio transports; harmless for docs / newer Cursor builds.
|
|
2580
2878
|
if (clientId === "cursor") {
|
|
@@ -2653,15 +2951,58 @@ async function main() {
|
|
|
2653
2951
|
output: process.stdout,
|
|
2654
2952
|
});
|
|
2655
2953
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2954
|
+
// Cloud or this machine, in a release WITH the cloud. `--local` / `--cloud` choose; otherwise the last install's
|
|
2955
|
+
// choice holds, which is what keeps `niro upgrade` (it re-runs this with only --client) from flipping it back.
|
|
2956
|
+
// The saved mode is Claude Code's alone, since local mode configures no other client yet: setting up
|
|
2957
|
+
// another client neither reads nor changes it (verifier, 2026-11-02: `--cloud` for Cursor used to
|
|
2958
|
+
// save cloud, and the next `niro upgrade` put Claude Code back on the cloud).
|
|
2959
|
+
// A release without the cloud (src/lib/release.js) has no choice to make: Claude Code is always set up for the
|
|
2960
|
+
// engine on this machine (setupClaudeCode), and every assistant goes through the picker below.
|
|
2961
|
+
if (require("../lib/release").cloud) {
|
|
2962
|
+
const asked = localMode.modeFromArgs(args);
|
|
2963
|
+
const forClaude = !clientArg || clientArg === "claude-code";
|
|
2964
|
+
const mode = forClaude ? localMode.chooseMode(args, config.load().mcpMode) : asked || "cloud";
|
|
2965
|
+
if (mode === "local") {
|
|
2966
|
+
rl.close();
|
|
2967
|
+
if (!forClaude) {
|
|
2968
|
+
console.error(`\n Local mode configures Claude Code only, so far; ${clientArg} is not supported yet.`);
|
|
2969
|
+
console.error(` Leave out --local to set ${clientArg} up for the cloud; Claude Code stays as it is.\n`);
|
|
2970
|
+
process.exit(1);
|
|
2971
|
+
}
|
|
2972
|
+
if (process.argv.includes("--list")) {
|
|
2973
|
+
console.log("\n Local mode configures Claude Code only, so far.\n");
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
if (!hasCliToken()) {
|
|
2977
|
+
console.log("\n You are not signed in. A development build of niro-local answers without it. A released");
|
|
2978
|
+
console.log(" one needs a licence, and so do the journal and audit uploads: run `niro login`.\n");
|
|
2979
|
+
}
|
|
2980
|
+
const done = await setupClaudeCode(null, null, {}, "local");
|
|
2981
|
+
if (done) {
|
|
2982
|
+
config.save({ mcpMode: "local" });
|
|
2983
|
+
// One step: `init` adds the folder and builds it (fix/init-builds). This branch runs only in a release WITH the
|
|
2984
|
+
// cloud, where `niro init` is the cloud's onboarding and local mode sits next to a niro-local the person runs
|
|
2985
|
+
// themselves (the same texts as src/mcp/localMode.js with CLOUD_RELEASE), so the engine's own command is the
|
|
2986
|
+
// one that works. A release without the cloud never reaches this: it has no mode to choose (see above).
|
|
2987
|
+
console.log(" The niro tools now answer from niro-local on this machine. Index a folder by running");
|
|
2988
|
+
console.log(" `niro-local init --project <name>` in it (it adds the folder and builds its graph).");
|
|
2989
|
+
console.log(" Back to the cloud: `niro mcp install --cloud`. Remove everything: `niro mcp uninstall`.");
|
|
2990
|
+
console.log(" Setup complete! Restart your AI assistant to start using Niro.\n");
|
|
2991
|
+
}
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
if (asked === "cloud" && forClaude) config.save({ mcpMode: "cloud" });
|
|
2995
|
+
}
|
|
2658
2996
|
// Credential: prefer a `niro login` session (cli-token, read at runtime from
|
|
2659
2997
|
// ~/.niro/credentials.json). Only prompt for an API key when there is no cli-token.
|
|
2660
2998
|
let apiKey = null;
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
console.
|
|
2664
|
-
|
|
2999
|
+
// Without the cloud there is no API key to paste: `niro login`'s browser flow (not `niro start`, which skips it for a saved key).
|
|
3000
|
+
if (!hasCliToken() && !require("../lib/release").cloud) {
|
|
3001
|
+
console.error(`\nNot signed in. Run ${require("../lib/color").cyan("niro login")}, approve in your browser, then try again.\n`);
|
|
3002
|
+
rl.close();
|
|
3003
|
+
process.exit(1);
|
|
3004
|
+
}
|
|
3005
|
+
if (!hasCliToken()) {
|
|
2665
3006
|
apiKey = await ask(rl, " Niro API key (from console.niroai.dev > Settings)", resolveApiKey());
|
|
2666
3007
|
if (!apiKey) {
|
|
2667
3008
|
console.error("\n No credential found. Run `niro login` first (recommended), or paste an API key");
|
|
@@ -2672,14 +3013,7 @@ async function main() {
|
|
|
2672
3013
|
}
|
|
2673
3014
|
|
|
2674
3015
|
// The MCP server URL is resolved, not prompted — see resolveMcpServerUrl.
|
|
2675
|
-
|
|
2676
|
-
const { url: mcpServerUrl, source: urlSource, explicit } = resolveMcpServerUrl(urlArg);
|
|
2677
|
-
console.log(` MCP server URL: ${mcpServerUrl} (${urlSource})`);
|
|
2678
|
-
if (!urlArg) {
|
|
2679
|
-
console.log(" Different backend? Re-run with: niro mcp install --url <url>\n");
|
|
2680
|
-
} else {
|
|
2681
|
-
console.log("");
|
|
2682
|
-
}
|
|
3016
|
+
const { url: mcpServerUrl, explicit } = resolveMcpServerUrl(urlArg);
|
|
2683
3017
|
|
|
2684
3018
|
// Persist an EXPLICITLY chosen URL to ~/.niro/config.json so the bridge can
|
|
2685
3019
|
// fall back to it and later installs reuse it. The default is never saved
|
|
@@ -2701,16 +3035,37 @@ async function main() {
|
|
|
2701
3035
|
return;
|
|
2702
3036
|
}
|
|
2703
3037
|
|
|
2704
|
-
// Select
|
|
2705
|
-
let
|
|
2706
|
-
if (
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
3038
|
+
// Select clients: one or more, comma- or space-separated.
|
|
3039
|
+
let selectedClients;
|
|
3040
|
+
if (clientArg) {
|
|
3041
|
+
selectedClients = parseClients(clientArg);
|
|
3042
|
+
} else {
|
|
3043
|
+
// readline would echo the arrow keys the picker reads, so it lets go of stdin first.
|
|
3044
|
+
rl.close();
|
|
3045
|
+
console.log(`\n${IND}Which AI assistants should use Niro?`);
|
|
3046
|
+
selectedClients = await choose({
|
|
3047
|
+
prefix: `${require("../lib/nestedOutput").currentPad()}${IND}`,
|
|
3048
|
+
items: [...Object.entries(CLIENTS).map(([value, c]) => ({ value, label: c.name })), { value: OTHER, label: "Other (print the config)" }],
|
|
3049
|
+
multi: true,
|
|
3050
|
+
initial: ["claude-code"],
|
|
3051
|
+
echo: false,
|
|
3052
|
+
});
|
|
3053
|
+
// The picker erased itself; the question goes too. The "Installed" block below names
|
|
3054
|
+
// what was picked.
|
|
3055
|
+
if (process.stdout.isTTY) process.stdout.write("\x1b[1A\r\x1b[2K");
|
|
3056
|
+
}
|
|
3057
|
+
// "Other": any MCP client this list does not know, so print the entry to paste and write nothing.
|
|
3058
|
+
if (selectedClients.includes(OTHER)) {
|
|
3059
|
+
selectedClients = selectedClients.filter((id) => id !== OTHER);
|
|
3060
|
+
printOtherConfig(apiKey, mcpServerUrl);
|
|
3061
|
+
if (!selectedClients.length) {
|
|
3062
|
+
rl.close();
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
2710
3065
|
}
|
|
2711
|
-
|
|
2712
|
-
if (!
|
|
2713
|
-
console.error(`\n Unknown client: ${
|
|
3066
|
+
const unknown = selectedClients.filter((id) => !CLIENTS[id]);
|
|
3067
|
+
if (!selectedClients.length || unknown.length) {
|
|
3068
|
+
console.error(`\n Unknown client: ${unknown.join(", ") || "(none given)"}`);
|
|
2714
3069
|
printClients();
|
|
2715
3070
|
rl.close();
|
|
2716
3071
|
process.exit(1);
|
|
@@ -2724,7 +3079,7 @@ async function main() {
|
|
|
2724
3079
|
// goose, which writes YAML directly with fs) never reaches a shell, so gate the
|
|
2725
3080
|
// shell-metacharacter guard to just these instead of blocking every client on a risk that
|
|
2726
3081
|
// only applies to some (e.g. a query-string URL with "&" is fine for Cursor/VS Code/etc.).
|
|
2727
|
-
if (CLI_SETUP[
|
|
3082
|
+
if (selectedClients.some((id) => CLI_SETUP[id])) {
|
|
2728
3083
|
if (hasShellMetacharacters(mcpServerUrl)) {
|
|
2729
3084
|
console.error(`\n Refusing to use MCP server URL "${mcpServerUrl}": it contains spaces or`);
|
|
2730
3085
|
console.error(" characters not allowed in a URL. Check --url / NIRO_MCP_SERVER_URL.\n");
|
|
@@ -2738,34 +3093,53 @@ async function main() {
|
|
|
2738
3093
|
}
|
|
2739
3094
|
// Goose writes YAML directly (no JSON shape, no shell) — its own dispatch branch, separate
|
|
2740
3095
|
// from both the shell-based CLI_SETUP map and the generic JSON setupJsonConfig path.
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
console.log
|
|
3096
|
+
// Each installer reports every file it touches; that detail is muted here and each
|
|
3097
|
+
// client gets one line. Errors (console.error) still come through.
|
|
3098
|
+
let success = true;
|
|
3099
|
+
const results = [];
|
|
3100
|
+
for (const selectedClient of selectedClients) {
|
|
3101
|
+
const log = console.log;
|
|
3102
|
+
console.log = () => {};
|
|
3103
|
+
installedSkills.length = 0;
|
|
3104
|
+
let ok;
|
|
3105
|
+
try {
|
|
3106
|
+
if (CLI_SETUP[selectedClient]) {
|
|
3107
|
+
ok = await CLI_SETUP[selectedClient](apiKey, mcpServerUrl);
|
|
3108
|
+
} else if (selectedClient === "goose") {
|
|
3109
|
+
ok = await setupGoose(apiKey, mcpServerUrl);
|
|
3110
|
+
} else {
|
|
3111
|
+
ok = await setupJsonConfig(selectedClient, apiKey, mcpServerUrl);
|
|
3112
|
+
}
|
|
3113
|
+
} finally {
|
|
3114
|
+
console.log = log;
|
|
3115
|
+
}
|
|
3116
|
+
results.push({ name: CLIENTS[selectedClient].name, ok: !!ok, dirs: installedSkills.map((k) => k.dir) });
|
|
3117
|
+
success = success && !!ok;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// One summary, like `npx skills`: what was installed, then where, per assistant.
|
|
3121
|
+
const installed = results.filter((r) => r.ok);
|
|
3122
|
+
if (installed.length) {
|
|
3123
|
+
console.log(`${IND}${color.bold(`Installed "niro" v${require("../../package.json").version}`)}`);
|
|
3124
|
+
const width = Math.max(...installed.map((r) => r.name.length));
|
|
3125
|
+
for (const r of installed) {
|
|
3126
|
+
const where = r.dirs.length ? r.dirs : ["connected"];
|
|
3127
|
+
where.forEach((d, i) => console.log(color.dim(`${IND} ${(i ? "" : r.name).padEnd(width)} → ${d}`)));
|
|
2760
3128
|
}
|
|
2761
3129
|
}
|
|
3130
|
+
for (const r of results.filter((x) => !x.ok)) console.log(`${IND}${color.red("✗")} ${r.name} could not be set up`);
|
|
3131
|
+
// None set up: say so in the exit code, so `niro start` does not go on to call the assistant connected.
|
|
3132
|
+
if (results.length && !installed.length) process.exitCode = 1;
|
|
3133
|
+
|
|
3134
|
+
// Inside `niro start` the step's closing line says this instead.
|
|
3135
|
+
if (success && !process.env.NIRO_OUTPUT_PREFIX) console.log("\n Restart your AI assistant to start using Niro.\n");
|
|
2762
3136
|
}
|
|
2763
3137
|
|
|
2764
3138
|
// Only run the interactive wizard when invoked directly (niro mcp install). When
|
|
2765
3139
|
// required as a module, expose the skill installers so they can be (re)installed
|
|
2766
3140
|
// on their own — without re-running the wizard, which would rewrite MCP config.
|
|
2767
3141
|
if (require.main === module) {
|
|
2768
|
-
|
|
3142
|
+
require("../lib/nestedOutput").withPrefix(process.env.NIRO_OUTPUT_PREFIX || "", main).catch((err) => {
|
|
2769
3143
|
console.error(`Setup failed: ${err.message}`);
|
|
2770
3144
|
process.exit(1);
|
|
2771
3145
|
});
|
|
@@ -2777,6 +3151,12 @@ module.exports = {
|
|
|
2777
3151
|
// resolver would drift the first time that order changed, and the two would then disagree
|
|
2778
3152
|
// about which backend a machine is pointed at.
|
|
2779
3153
|
resolveMcpServerUrl,
|
|
3154
|
+
// Exported so `niro upgrade` warns exactly when this installer would refuse (or ask for a key):
|
|
3155
|
+
// a second "is this machine signed in?" rule drifted from this one and stayed silent after
|
|
3156
|
+
// `niro login --api-key`, which saves a key but no cli token.
|
|
3157
|
+
hasCliToken,
|
|
3158
|
+
parseClients,
|
|
3159
|
+
printOtherConfig,
|
|
2780
3160
|
writeManagedSkill,
|
|
2781
3161
|
addClaudeCodeSkill,
|
|
2782
3162
|
addClaudeCodeCliSkill,
|
|
@@ -2788,6 +3168,9 @@ module.exports = {
|
|
|
2788
3168
|
gooseConfigPath,
|
|
2789
3169
|
NIRO_MD_MARKER,
|
|
2790
3170
|
NIRO_MD_SECTIONS,
|
|
3171
|
+
localText,
|
|
3172
|
+
localTextBeforeUsingNiro,
|
|
3173
|
+
NIRO_ALL_MD_SECTIONS,
|
|
2791
3174
|
isNiroPostCompactHook,
|
|
2792
3175
|
isNiroUserPromptHook,
|
|
2793
3176
|
isNiroPreToolUseHook,
|
|
@@ -2798,6 +3181,9 @@ module.exports = {
|
|
|
2798
3181
|
planClaudeRegistration,
|
|
2799
3182
|
registeredServerUrl,
|
|
2800
3183
|
isNiroStopHook,
|
|
3184
|
+
isNiroLineCheckHook,
|
|
3185
|
+
addClaudeCodeLineCheckHook,
|
|
3186
|
+
NIRO_LINE_CHECK_COMMAND,
|
|
2801
3187
|
clearCodexPerToolApprovals,
|
|
2802
3188
|
hardenCodexServerConfig,
|
|
2803
3189
|
userHome,
|
|
@@ -2821,6 +3207,8 @@ module.exports = {
|
|
|
2821
3207
|
NIRO_EXPLORE_FIRST_SENTENCE,
|
|
2822
3208
|
NIRO_IMPACT_BOTH_WAYS_SENTENCE,
|
|
2823
3209
|
NIRO_CURSOR_RULE,
|
|
3210
|
+
refreshCursorRule,
|
|
3211
|
+
bridgeEnv,
|
|
2824
3212
|
NIRO_CLINE_RULE,
|
|
2825
3213
|
NIRO_KIRO_STEERING,
|
|
2826
3214
|
upsertNiroMarkerSection,
|