@niroai/niro 0.11.0 → 0.13.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 +128 -0
- package/README.md +72 -3
- package/docs/working-on-branches.md +8 -0
- package/package.json +1 -1
- package/src/commands/_resolveProject.js +59 -3
- package/src/commands/delete.js +16 -1
- package/src/commands/mcp.js +16 -7
- package/src/commands/remove.js +13 -0
- package/src/commands/start.js +1 -0
- package/src/commands/upgrade.js +400 -0
- package/src/index.js +11 -0
- package/src/lib/folderSync.js +1 -0
- package/src/lib/impactLog.js +251 -0
- package/src/lib/mcpClients.js +241 -0
- package/src/lib/receiptStore.js +1 -1
- package/src/lib/service.js +18 -1
- package/src/mcp/server.js +30 -10
- package/src/mcp/setup.js +273 -17
- package/src/mcp/teardown.js +7 -2
package/src/mcp/setup.js
CHANGED
|
@@ -50,7 +50,7 @@ const NIRO_MD_MARKER = "<!-- niro-mcp -->";
|
|
|
50
50
|
* read once per session, which is why it now states its own non-expiry explicitly. Wording is imperative
|
|
51
51
|
* and names concrete tools so the agent knows what to call instead of native search.
|
|
52
52
|
*/
|
|
53
|
-
const NIRO_EXPLORE_FIRST_SENTENCE = "MUST FOLLOW: before you run grep, find, glob, or open a file to hunt for something, first check whether a Niro tool answers it (code_grep for content search, resolve_fqn and get_source_code for a known symbol, find_handler and find_api_endpoints for entry points). Use the connection tools (get_call_chain, get_reverse_call_chain, get_blast_radius, get_impact_analysis, get_service_topology) to understand how code connects BEFORE deciding which file to read or search next, rather than reading files to rebuild that picture by hand. Native search is the fallback for what Niro does not cover (config files, docs, logs, generated output, non-code text) or when the tools are unavailable. This rule does not expire: it holds on the first turn and on the fiftieth, mid-task as much as at the start of a session, and a long conversation is not a reason to drift back to native search.";
|
|
53
|
+
const NIRO_EXPLORE_FIRST_SENTENCE = "MUST FOLLOW: before you run grep, find, glob, or open a file to hunt for something, first check whether a Niro tool answers it (code_grep for content search, resolve_fqn and get_source_code for a known symbol, find_handler and find_api_endpoints for entry points). Use the connection tools (get_call_chain, get_reverse_call_chain, get_blast_radius, get_impact_analysis, get_service_topology, get_exception_flow for which exceptions can escape a method) to understand how code connects BEFORE deciding which file to read or search next, rather than reading files to rebuild that picture by hand. Native search is the fallback for what Niro does not cover (config files, docs, logs, generated output, non-code text) or when the tools are unavailable. This rule does not expire: it holds on the first turn and on the fiftieth, mid-task as much as at the start of a session, and a long conversation is not a reason to drift back to native search.";
|
|
54
54
|
|
|
55
55
|
const NIRO_MULTI_REPO_SENTENCE = "The temporary project answer is per (repo, BRANCH): after you create/switch a branch in a repo — and before your first edit on that branch — call should_create_temp_project with the repo's git url, its CURRENT branch, and its `.niro-project` id if present, then run `niro new-temp-project` in its folder if needs_temp_project is true. Re-check after every branch change (a `false` from another branch does NOT carry over); this applies to every repo you touch, including when the session started in a parent folder that is not itself a git repo.";
|
|
56
56
|
/**
|
|
@@ -63,7 +63,7 @@ const NIRO_MULTI_REPO_SENTENCE = "The temporary project answer is per (repo, BRA
|
|
|
63
63
|
* is agreed the wrong basis is already baked in; and it says the size of the diff does not excuse it,
|
|
64
64
|
* because a one-line change to shared code reaches further than a rewrite of a standalone tool.
|
|
65
65
|
*/
|
|
66
|
-
const NIRO_IMPACT_BOTH_WAYS_SENTENCE = "MUST FOLLOW: before you plan, propose, or start any change to existing code, establish BOTH sides of it. Upstream: what depends on the code you are about to touch, including callers in other services over HTTP or events, and the tests and config that name it. Downstream: what that code itself calls, and the data and events it relies on. Use get_blast_radius, which returns callers, callees, cross-service callers and impacted files in one call, or get_impact_analysis for the lighter upstream-only view. Put the result in the plan you show the user: name the callers you found, say which ones you will update, and say which ones you are deliberately leaving alone. This applies to a one-line change too, because how far a change reaches is decided by what depends on it, not by the size of the diff.";
|
|
66
|
+
const NIRO_IMPACT_BOTH_WAYS_SENTENCE = "MUST FOLLOW: before you plan, propose, or start any change to existing code, establish BOTH sides of it. Upstream: what depends on the code you are about to touch, including callers in other services over HTTP or events, and the tests and config that name it. Downstream: what that code itself calls, and the data and events it relies on. Use get_blast_radius, which returns callers, callees, cross-service callers and impacted files in one call, or get_impact_analysis for the lighter upstream-only view. Put the result in the plan you show the user: name the callers you found, say which ones you will update, and say which ones you are deliberately leaving alone. This applies to a one-line change too, because how far a change reaches is decided by what depends on it, not by the size of the diff. Keep that list: after the edit, verify those flows still hold (run their tests, trace them, read the callers) and say what you verified. The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify; use the list you captured before. A change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so in one sentence.";
|
|
67
67
|
const NIRO_CLAUDE_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP tools are available for code intelligence. Use them (call chains, blast radius, impact analysis, source lookup) before falling back to native grep/read for code questions.\nBefore adding/writing any new code, you MUST use the find_reusable_code tool from niro to figure out if there is anything reusable.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_IMPACT_BOTH_WAYS_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
|
|
68
68
|
const NIRO_CODEX_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools and the \`niro-skill\` are available (call chains, blast radius, impact analysis, source lookup). Prefer them over grep/read for questions about this codebase; the niro-skill has the full tool-routing playbook. 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`;
|
|
69
69
|
const NIRO_WINDSURF_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools and the niro-skill are available. Prefer them over grep/read for questions about this codebase (structure, call chains, blast radius, impact, data flows, reuse); the niro-skill has the full routing playbook. Before writing any new code, use find_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`;
|
|
@@ -161,6 +161,19 @@ function isNiroExploreHook(h) {
|
|
|
161
161
|
return cmds.some((c) => c.includes("niro-explore-"));
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* True if `h` is niro's PreToolUse impact-gate entry (matcher Edit|Write). Keyed on the unique
|
|
166
|
+
* `niro-impact-` identity token so a foreign PreToolUse hook on the same matcher, and niro's own
|
|
167
|
+
* temp-project hook on the same matcher, are never matched or removed by mistake. Shared by installer
|
|
168
|
+
* (upgrade-in-place) and uninstaller (removal).
|
|
169
|
+
*/
|
|
170
|
+
function isNiroImpactHook(h) {
|
|
171
|
+
const cmds = [];
|
|
172
|
+
if (h && Array.isArray(h.hooks)) h.hooks.forEach((i) => i && typeof i.command === "string" && cmds.push(i.command));
|
|
173
|
+
if (h && typeof h.command === "string") cmds.push(h.command);
|
|
174
|
+
return cmds.some((c) => c.includes("niro-impact-"));
|
|
175
|
+
}
|
|
176
|
+
|
|
164
177
|
/**
|
|
165
178
|
* Writes a niro-managed skill/rule file, refreshing it when the generated content has
|
|
166
179
|
* changed. These files are GENERATED (not hand-edited), so refresh-on-change is correct:
|
|
@@ -589,8 +602,49 @@ function printClients({ all = false } = {}) {
|
|
|
589
602
|
console.log("");
|
|
590
603
|
}
|
|
591
604
|
|
|
592
|
-
|
|
605
|
+
/** The URL an existing Claude Code registration points at, read off `claude mcp get niro`; null if none. */
|
|
606
|
+
function registeredServerUrl(getOutput) {
|
|
607
|
+
const m = /NIRO_MCP_SERVER_URL=(\S+)/.exec(String(getOutput || ""));
|
|
608
|
+
return m ? m[1] : null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* What to do about the Claude Code registration before the hooks are written. Pure, so it is tested
|
|
613
|
+
* with the real `claude mcp get` output as a fixture.
|
|
614
|
+
* "add" nothing is registered (`claude mcp get niro` failed)
|
|
615
|
+
* "keep" registered and already pointing at the wanted URL
|
|
616
|
+
* "replace" registered, but at another URL or in a shape this cannot read: remove, then add
|
|
617
|
+
*/
|
|
618
|
+
function planClaudeRegistration({ existingOutput, wantedUrl }) {
|
|
619
|
+
if (existingOutput == null) return "add";
|
|
620
|
+
const url = registeredServerUrl(existingOutput);
|
|
621
|
+
return url && url === wantedUrl ? "keep" : "replace";
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Register the niro server with Claude Code, or keep the registration already there, then write the
|
|
626
|
+
* hooks, the memory section and the skills.
|
|
627
|
+
*
|
|
628
|
+
* `claude mcp add` refuses when a server of that name exists ("MCP server niro already exists in
|
|
629
|
+
* user config", exit 1). This function treated every failure of that command as "Claude Code is not
|
|
630
|
+
* installed" and returned before writing a single hook, so on any machine where niro had been set up
|
|
631
|
+
* once, `niro mcp install` refreshed nothing: not the hooks, not the CLAUDE.md section. That is the
|
|
632
|
+
* exact step the out-of-date notice tells users to run. Found 2026-09-05 when the new Edit|Write
|
|
633
|
+
* hook could not be installed on the machine it was written on.
|
|
634
|
+
*
|
|
635
|
+
* `deps` is for tests only: `get` returns the `claude mcp get niro` output or null, `run` runs a
|
|
636
|
+
* client command; both default to the real thing.
|
|
637
|
+
*/
|
|
638
|
+
async function setupClaudeCode(apiKey, mcpServerUrl, deps = {}) {
|
|
593
639
|
console.log("\n Configuring Claude Code...\n");
|
|
640
|
+
const run = deps.run || runClientCommand;
|
|
641
|
+
const get = deps.get || (() => {
|
|
642
|
+
try {
|
|
643
|
+
return execSync("claude mcp get niro", { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
644
|
+
} catch (_) {
|
|
645
|
+
return null; // not registered, or no `claude` on the PATH: `add` below tells the two apart
|
|
646
|
+
}
|
|
647
|
+
});
|
|
594
648
|
|
|
595
649
|
const cmd = [
|
|
596
650
|
"claude", "mcp", "add",
|
|
@@ -604,20 +658,34 @@ async function setupClaudeCode(apiKey, mcpServerUrl) {
|
|
|
604
658
|
cmd.push("-e", `NIRO_MCP_SERVER_URL=${mcpServerUrl}`);
|
|
605
659
|
cmd.push("--", "niro", "mcp", "serve");
|
|
606
660
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
console.log(
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
661
|
+
const plan = planClaudeRegistration({ existingOutput: get(), wantedUrl: mcpServerUrl });
|
|
662
|
+
if (plan === "keep") {
|
|
663
|
+
console.log(` MCP server already registered in Claude Code at ${mcpServerUrl}. Keeping it.\n`);
|
|
664
|
+
} else {
|
|
665
|
+
if (plan === "replace") {
|
|
666
|
+
try {
|
|
667
|
+
run("claude mcp remove --scope user niro");
|
|
668
|
+
} catch (_) {
|
|
669
|
+
/* the add below reports what is wrong */
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
run(cmd.join(" "));
|
|
674
|
+
console.log(" MCP server registered globally in Claude Code.\n");
|
|
675
|
+
} catch (err) {
|
|
676
|
+
const detail = String((err && (err.stderr || err.message)) || "").trim().split("\n")[0];
|
|
677
|
+
console.error(` Failed to register MCP server${detail ? `: ${detail}` : ""}.`);
|
|
678
|
+
console.error(" If Claude Code is not installed: https://claude.ai/code\n");
|
|
679
|
+
console.error(` Manual command:\n ${cmd.join(" ")}\n`);
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
615
682
|
}
|
|
616
683
|
|
|
617
684
|
addClaudeCodePostCompactHook();
|
|
618
685
|
addClaudeCodeUserPromptHook();
|
|
619
686
|
addClaudeCodePreToolUseHook();
|
|
620
687
|
addClaudeCodeExploreHook();
|
|
688
|
+
addClaudeCodeImpactHook();
|
|
621
689
|
removeStaleNiroStopHook();
|
|
622
690
|
addClaudeCodeMemory();
|
|
623
691
|
addClaudeCodeSkill();
|
|
@@ -670,7 +738,9 @@ function isNiroGeneratedPostCompactHook(h) {
|
|
|
670
738
|
const NIRO_POST_COMPACT_COMMAND =
|
|
671
739
|
"echo 'Niro MCP tools are available for code intelligence (call chains, blast radius, impact analysis, source lookup). "
|
|
672
740
|
+ "Use niro tools before native grep/read for code questions. Before you plan or propose a change to existing code, call "
|
|
673
|
-
+ "get_blast_radius (or get_impact_analysis) to see who calls it and what it calls, and put that impact in the plan.
|
|
741
|
+
+ "get_blast_radius (or get_impact_analysis) to see who calls it and what it calls, and put that impact in the plan. "
|
|
742
|
+
+ "Keep that list and verify those flows after the edit; the graph lags your edit, so do not re-query it to verify. "
|
|
743
|
+
+ "A log line or comment needs no map.'";
|
|
674
744
|
|
|
675
745
|
function addClaudeCodePostCompactHook() {
|
|
676
746
|
// Guard the invariant that keeps upgrade-in-place working: if the reminder is ever reworded so it
|
|
@@ -781,7 +851,7 @@ function addClaudeCodeMemory() {
|
|
|
781
851
|
*/
|
|
782
852
|
const NIRO_USER_PROMPT_COMMAND =
|
|
783
853
|
"IN=\"$(cat)\"; d=\"${CLAUDE_PROJECT_DIR:-$PWD}\"; while [ -n \"$d\" ] && [ \"$d\" != \"/\" ]; do " +
|
|
784
|
-
"if [ -f \"$d/.niro-project\" ]; then echo 'NIRO ACTIVE. Standing rule for this whole session, at the start and mid-task alike: before Grep/Glob/Read or any native file hunt, use niro (code_grep, get_source_code, resolve_fqn, get_call_chain, get_blast_radius, get_impact_analysis). Before writing new code, call find_reusable_code. Before you plan or propose a change to existing code, call get_blast_radius (or get_impact_analysis) to see who calls it and what it calls, and put that impact in the plan. Native tools only for config, docs, logs, generated or non-code text. If the niro tools are not in your toolset at all, use native tools and do not mention Niro.'; " +
|
|
854
|
+
"if [ -f \"$d/.niro-project\" ]; then echo 'NIRO ACTIVE. Standing rule for this whole session, at the start and mid-task alike: before Grep/Glob/Read or any native file hunt, use niro (code_grep, get_source_code, resolve_fqn, get_call_chain, get_blast_radius, get_impact_analysis). Before writing new code, call find_reusable_code. Before you plan or propose a change to existing code, call get_blast_radius (or get_impact_analysis) to see who calls it and what it calls, and put that impact in the plan. Keep that list and verify those flows after the edit; the graph lags your edit, so do not re-query it to verify. A log line or comment needs no map. Native tools only for config, docs, logs, generated or non-code text. If the niro tools are not in your toolset at all, use native tools and do not mention Niro.'; " +
|
|
785
855
|
"printf '%s' \"$IN\" | grep -qiE 'new branch|create[^.]{0,24}branch|switch[^.]{0,24}branch|checkout -b|change[^.]{0,24}branch|different branch|another branch' && " +
|
|
786
856
|
"echo 'You appear to be about to create or switch a branch. The temporary project check is per (repo, branch), so do it AFTER you are on the new branch (not before) and pass that CURRENT branch: call should_create_temp_project(git_url, branch, project_id from its .niro-project) and run niro new-temp-project if needs_temp_project is true. A check you ran on the old branch does not count.'; break; fi; " +
|
|
787
857
|
"d=\"$(dirname \"$d\")\"; done";
|
|
@@ -1102,6 +1172,165 @@ function addClaudeCodeExploreHook() {
|
|
|
1102
1172
|
}
|
|
1103
1173
|
}
|
|
1104
1174
|
|
|
1175
|
+
/**
|
|
1176
|
+
* The three answers the impact hook can give. Built with JSON.stringify so the text is valid JSON by
|
|
1177
|
+
* construction. All three are wrapped in single quotes inside the shell command, so no message may
|
|
1178
|
+
* contain an apostrophe or a backtick; the test suite executes the command, which would break loudly
|
|
1179
|
+
* if one crept in. None carries a permissionDecision: the hook advises, it never approves or refuses.
|
|
1180
|
+
*
|
|
1181
|
+
* Why advisory and not a refusal, in Niraj's words (2026-09-05): "The reason I said before editing
|
|
1182
|
+
* any method, the impact of editing it should be measured is because I want the coding agent to verify
|
|
1183
|
+
* that after the change as well, the flows connected with the current method are not breaking. [...]
|
|
1184
|
+
* graph updating takes time after an edit is performed. The change does not take effect instantly.
|
|
1185
|
+
* [...] it is not to enforce use of niro [...] now if someone is just adding a log message, that's not
|
|
1186
|
+
* gonna affect anything so if the verification should happen or not is also subjective."
|
|
1187
|
+
*/
|
|
1188
|
+
const NIRO_IMPACT_UNMAPPED_JSON = JSON.stringify({
|
|
1189
|
+
hookSpecificOutput: {
|
|
1190
|
+
hookEventName: "PreToolUse",
|
|
1191
|
+
additionalContext:
|
|
1192
|
+
"Niro impact check, before you edit this file: decide whether this change can alter behaviour. " +
|
|
1193
|
+
"A log line, a comment, formatting or a message string cannot, so say so in one sentence and go " +
|
|
1194
|
+
"ahead. Anything else can, so map who depends on the method NOW with get_blast_radius or " +
|
|
1195
|
+
"get_impact_analysis (a partial name such as ClassName.method resolves when it is unique; " +
|
|
1196
|
+
"resolve_fqn on one that does not) and keep the list of callers and flows: it is your checklist " +
|
|
1197
|
+
"for AFTER the edit. The graph updates only after your edit is synced and rebuilt, so do not " +
|
|
1198
|
+
"re-query it to verify; verify the listed flows by running their tests, tracing them, or reading " +
|
|
1199
|
+
"the callers, and say what you verified. Shown once per file per session.",
|
|
1200
|
+
},
|
|
1201
|
+
});
|
|
1202
|
+
const NIRO_IMPACT_MAPPED_JSON = JSON.stringify({
|
|
1203
|
+
hookSpecificOutput: {
|
|
1204
|
+
hookEventName: "PreToolUse",
|
|
1205
|
+
additionalContext:
|
|
1206
|
+
"Niro impact check: an impact map for this file was recorded in the last hour. After this edit, " +
|
|
1207
|
+
"go back to the callers and flows it listed and verify they still hold: run their tests, trace " +
|
|
1208
|
+
"them, or read the callers, and say what you verified. The graph updates only after your edit is " +
|
|
1209
|
+
"synced and rebuilt, so a fresh query would still show the old code; use the list you already " +
|
|
1210
|
+
"have. Shown once per file per session.",
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
const NIRO_IMPACT_NO_BRIDGE_JSON = JSON.stringify({
|
|
1214
|
+
hookSpecificOutput: {
|
|
1215
|
+
hookEventName: "PreToolUse",
|
|
1216
|
+
additionalContext:
|
|
1217
|
+
"Niro impact check: you are editing indexed source with no impact map recorded for this file, " +
|
|
1218
|
+
"and the Niro bridge has not answered in the last 30 minutes. If this change can alter behaviour " +
|
|
1219
|
+
"(anything beyond a log line, a comment, formatting or a message string), map who depends on it " +
|
|
1220
|
+
"as soon as Niro answers and verify those flows after the edit; the graph will lag your edit, so " +
|
|
1221
|
+
"use the list you capture, not a fresh query. Shown once per file per session.",
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* The niro PreToolUse hook command (matcher Edit|Write): before an edit to indexed source, make the
|
|
1227
|
+
* agent decide out loud whether the change can alter behaviour and, if so, map who depends on the
|
|
1228
|
+
* method while the graph is still current, then carry that list across the edit as the checklist for
|
|
1229
|
+
* afterwards. The prose rule (NIRO_IMPACT_BOTH_WAYS_SENTENCE) is on every instruction surface and
|
|
1230
|
+
* still gets read past; a reminder at the moment of the edit is what survives a long session.
|
|
1231
|
+
* Full design, the reasoning in Niraj's words, and the scenario table: docs/usecases/impact-gate.md.
|
|
1232
|
+
*
|
|
1233
|
+
* Advisory, never a refusal. The first cut denied the edit until an impact call named the file. That
|
|
1234
|
+
* enforced a tool call, which was never the goal; it could not tell a log line from a signature change;
|
|
1235
|
+
* and review found two ways it failed closed. Now every answer is additionalContext, and it never emits
|
|
1236
|
+
* permissionDecision at all ("allow" would auto-approve every edit and bypass the permission prompt).
|
|
1237
|
+
*
|
|
1238
|
+
* The evidence comes from the bridge: src/lib/impactLog.js writes one JSON line per answered
|
|
1239
|
+
* get_blast_radius / get_impact_analysis / get_reverse_call_chain call to ~/.niro/impact/<key>.jsonl,
|
|
1240
|
+
* and touches <key>.alive whenever the server answers. <key> is the session folder with every
|
|
1241
|
+
* non-alphanumeric byte replaced by "-": here `cd -P` + `tr -c`, there fs.realpathSync + a byte loop,
|
|
1242
|
+
* and test/impactGateHook.test.js proves the two agree.
|
|
1243
|
+
*
|
|
1244
|
+
* Decision, top to bottom, first match wins: no file_path, the off switch, a file that does not exist
|
|
1245
|
+
* yet, a non-source extension, a test path, or no `.niro-project` above the file -> silent. A log line
|
|
1246
|
+
* from the last hour that names the file (a files entry ending "/Foo.java", or a name containing
|
|
1247
|
+
* ".Foo.", "\"Foo.", ".Foo\"", "\"Foo\"," or "\"Foo\"]": always a VALUE, so a file called ts.js or
|
|
1248
|
+
* tool.js is not matched by the record's own "ts"/"tool" keys) -> the MAPPED reminder: verify the
|
|
1249
|
+
* listed flows after the edit, do not re-query the lagging graph. Otherwise, alive file touched in the
|
|
1250
|
+
* last 30 minutes -> the UNMAPPED reminder: decide, and map now if the change can alter behaviour; no
|
|
1251
|
+
* live bridge -> the short NO_BRIDGE form. Each reminder is shown once per (session, file), keyed like
|
|
1252
|
+
* the temp-project hook's marker, so ten edits to one file do not produce ten copies.
|
|
1253
|
+
*
|
|
1254
|
+
* Dependency-free like its siblings (grep -oE, sed -E, tr, find -mmin, cksum: GNU + BSD).
|
|
1255
|
+
* `niro-impact-` is the identity token isNiroImpactHook and teardown match on.
|
|
1256
|
+
*/
|
|
1257
|
+
const NIRO_IMPACT_COMMAND = [
|
|
1258
|
+
'IN="$(cat)"',
|
|
1259
|
+
`f="$(printf '%s' "$IN" | grep -oE '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
|
|
1260
|
+
'[ -z "$f" ] && exit 0',
|
|
1261
|
+
`c="$(printf '%s' "$IN" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
|
|
1262
|
+
'[ -z "$c" ] && c="${CLAUDE_PROJECT_DIR:-$PWD}"',
|
|
1263
|
+
'case "$f" in /*) ;; *) f="$c/$f" ;; esac',
|
|
1264
|
+
'H="${HOME:-/tmp}"',
|
|
1265
|
+
'[ -f "$H/.niro/impact-gate-off" ] && exit 0',
|
|
1266
|
+
// A file that does not exist yet has no callers; a Write creating it is never gated.
|
|
1267
|
+
'[ -f "$f" ] || exit 0',
|
|
1268
|
+
'case "$f" in *.java|*.js|*.mjs|*.cjs|*.ts|*.tsx|*.jsx|*.py|*.go|*.rb|*.kt|*.kts|*.cs|*.php|*.rs|*.scala|*.swift|*.c|*.cc|*.cpp|*.h|*.hpp) ;; *) exit 0 ;; esac',
|
|
1269
|
+
'case "$f" in */test/*|*/tests/*|*/__tests__/*|*/spec/*|*.test.*|*.spec.*|*Test.*|*Tests.*|*IT.java|*_test.*|*/test_*|*_spec.*|*/conftest.py) exit 0 ;; esac',
|
|
1270
|
+
// Symlink-resolved walk-up for the .niro-project marker, same as the explore hook.
|
|
1271
|
+
`d0="$(dirname "$f")"; d0="$(cd -P "$d0" 2>/dev/null && pwd -P || printf '%s' "$d0")"`,
|
|
1272
|
+
'd="$d0"; ok=0; while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "." ]; do if [ -f "$d/.niro-project" ]; then ok=1; break; fi; p="$(dirname "$d")"; [ "$p" = "$d" ] && break; d="$p"; done',
|
|
1273
|
+
'[ "$ok" = 0 ] && exit 0',
|
|
1274
|
+
// The log key, byte for byte what impactLog.keyForCwd computes from the bridge's process.cwd().
|
|
1275
|
+
`cr="$(cd -P "$c" 2>/dev/null && pwd -P || printf '%s' "$c")"`,
|
|
1276
|
+
`k="$(printf '%s' "$cr" | tr -c 'A-Za-z0-9' '-')"`,
|
|
1277
|
+
'L="$H/.niro/impact/$k.jsonl"; A="$H/.niro/impact/$k.alive"',
|
|
1278
|
+
'b="$(basename "$f")"; n="${b%.*}"',
|
|
1279
|
+
// The session + file key for the once-per-file markers, hashed the way the temp-project hook does.
|
|
1280
|
+
`s="$(printf '%s' "$IN" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
|
|
1281
|
+
`j="$(printf '%s\\n%s' "$s" "$f" | cksum | cut -d' ' -f1)"`,
|
|
1282
|
+
'NIRO_HOOK_ID=niro-impact-gate',
|
|
1283
|
+
// Mapped: the log was written in the last hour (the bridge prunes on write, so an older file holds
|
|
1284
|
+
// only stale lines) and a line names this file or its class -> remind once to verify AFTER the edit.
|
|
1285
|
+
`if [ -f "$L" ] && [ -n "$(find "$L" -mmin -60 2>/dev/null)" ] && grep -qF -e "/$b\\"" -e ".$n." -e "\\"$n." -e ".$n\\"" -e "\\"$n\\"," -e "\\"$n\\"]" "$L" 2>/dev/null; then m="\${TMPDIR:-/tmp}/niro-impact-after-$j"; [ -f "$m" ] && exit 0; touch "$m"; printf '%s' '${NIRO_IMPACT_MAPPED_JSON}'; exit 0; fi`,
|
|
1286
|
+
// Unmapped: ask once per (session, file) for the decision; with no live bridge, the short form.
|
|
1287
|
+
'm="${TMPDIR:-/tmp}/niro-impact-before-$j"; [ -f "$m" ] && exit 0; touch "$m"',
|
|
1288
|
+
`if [ -f "$A" ] && [ -n "$(find "$A" -mmin -30 2>/dev/null)" ]; then printf '%s' '${NIRO_IMPACT_UNMAPPED_JSON}'; else printf '%s' '${NIRO_IMPACT_NO_BRIDGE_JSON}'; fi`,
|
|
1289
|
+
].join('; ');
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Adds the PreToolUse (Edit|Write) impact-gate hook to ~/.claude/settings.json. Idempotent: upgrades a
|
|
1293
|
+
* stale niro entry in place, never touches a foreign PreToolUse hook on the same matcher nor niro's own
|
|
1294
|
+
* temp-project hook (matched by the niro-impact- token via isNiroImpactHook).
|
|
1295
|
+
*/
|
|
1296
|
+
function addClaudeCodeImpactHook() {
|
|
1297
|
+
const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
1298
|
+
let settings = {};
|
|
1299
|
+
try {
|
|
1300
|
+
if (fs.existsSync(settingsPath)) settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
1301
|
+
} catch {
|
|
1302
|
+
// Corrupt or missing — start fresh
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (!settings.hooks) settings.hooks = {};
|
|
1306
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
1307
|
+
|
|
1308
|
+
const entry = { matcher: "Edit|Write", hooks: [{ type: "command", command: NIRO_IMPACT_COMMAND }] };
|
|
1309
|
+
const alreadyCurrent = settings.hooks.PreToolUse.some(
|
|
1310
|
+
(h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === NIRO_IMPACT_COMMAND),
|
|
1311
|
+
);
|
|
1312
|
+
if (alreadyCurrent) {
|
|
1313
|
+
console.log(" PreToolUse impact-gate hook already up to date. Skipping.\n");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const hadStale = settings.hooks.PreToolUse.some(isNiroImpactHook);
|
|
1318
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !isNiroImpactHook(h));
|
|
1319
|
+
settings.hooks.PreToolUse.push(entry);
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
1323
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1324
|
+
console.log(` PreToolUse impact-gate hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
|
|
1325
|
+
console.log(" Before an edit to indexed source, Claude is asked to map who depends on the method and to");
|
|
1326
|
+
console.log(" verify those flows after the edit. Advisory only. Off switch: touch ~/.niro/impact-gate-off\n");
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
|
|
1329
|
+
console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
|
|
1330
|
+
console.warn(JSON.stringify(entry, null, 2) + "\n");
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1105
1334
|
/** Body of the `niro` skill (long-form tool-routing playbook). */
|
|
1106
1335
|
const NIRO_SKILL = `---
|
|
1107
1336
|
name: niro-skill
|
|
@@ -1143,6 +1372,12 @@ plan, propose, or start any change to existing code, establish both sides:
|
|
|
1143
1372
|
impacted files). \`get_impact_analysis\` is the lighter upstream-only view. Pass
|
|
1144
1373
|
\`include_source_code=true\` when you need to read what you found.
|
|
1145
1374
|
|
|
1375
|
+
Keep the list. After the edit, go back to the callers and flows it named and verify they still
|
|
1376
|
+
hold: run their tests, trace them, read the callers, and say what you verified. The graph updates
|
|
1377
|
+
only after your edit is synced and rebuilt, so do not re-query it to verify; use the list you
|
|
1378
|
+
captured before. A change that cannot alter behaviour at all (a log line, a comment, formatting)
|
|
1379
|
+
needs no map: say so in one sentence.
|
|
1380
|
+
|
|
1146
1381
|
Put the answer in the plan you show the user, not just in your head: name the callers you found,
|
|
1147
1382
|
say which ones you will update, and say which ones you are deliberately leaving alone. A one-line
|
|
1148
1383
|
change is not exempt, because reach is decided by what depends on the code, not by the size of the
|
|
@@ -1301,7 +1536,8 @@ Also available (catalog / utility, use when the above don't fit): \`list_project
|
|
|
1301
1536
|
- **Before adding or writing ANY new code, call \`find_reusable_code\` first.** Mandatory.
|
|
1302
1537
|
- **Before you plan or propose a change to existing code, call \`get_blast_radius\` (or
|
|
1303
1538
|
\`get_impact_analysis\`) and put the upstream and downstream impact in the plan.** Mandatory,
|
|
1304
|
-
whatever the size of the change.
|
|
1539
|
+
whatever the size of the change. Keep the list and verify those flows after the edit; the graph
|
|
1540
|
+
lags your edit, so do not re-query it to verify. A log line or comment needs no map.
|
|
1305
1541
|
- For comprehension / debugging, set \`purpose\` to COMPREHEND or DEBUG and, on
|
|
1306
1542
|
\`get_blast_radius\` / \`get_impact_analysis\`, pass \`include_source_code=true\`.
|
|
1307
1543
|
- If a result contains **⚠️ ACTION REQUIRED**, the graph is stale — read the listed files
|
|
@@ -1559,7 +1795,11 @@ any change to existing code, establish who depends on it (upstream callers, incl
|
|
|
1559
1795
|
services over HTTP or events) and what it depends on (downstream callees, data, events). Use
|
|
1560
1796
|
get_blast_radius, which returns both sides plus impacted files in one call, or
|
|
1561
1797
|
get_impact_analysis for the upstream-only view. Name the callers you found in the plan and say
|
|
1562
|
-
which ones you will update. A one-line change is not exempt.
|
|
1798
|
+
which ones you will update. A one-line change is not exempt. Keep that list: after the edit, verify
|
|
1799
|
+
those flows still hold (run their tests, trace them, read the callers) and say what you verified.
|
|
1800
|
+
The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify. A
|
|
1801
|
+
change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
|
|
1802
|
+
in one sentence.
|
|
1563
1803
|
|
|
1564
1804
|
Availability self-check FIRST — if the niro tools are not present in this session, or the
|
|
1565
1805
|
repo is not indexed (no project resolves / a tool reports "not indexed"), fall back to
|
|
@@ -2029,7 +2269,11 @@ any change to existing code, establish who depends on it (upstream callers, incl
|
|
|
2029
2269
|
other services over HTTP or events) and what it depends on (downstream callees, data, events). Use
|
|
2030
2270
|
\`get_blast_radius\`, which returns both sides plus impacted files in one call, or
|
|
2031
2271
|
\`get_impact_analysis\` for the upstream-only view. Name the callers you found in the plan and say
|
|
2032
|
-
which ones you will update. A one-line change is not exempt.
|
|
2272
|
+
which ones you will update. A one-line change is not exempt. Keep that list: after the edit, verify
|
|
2273
|
+
those flows still hold (run their tests, trace them, read the callers) and say what you verified.
|
|
2274
|
+
The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify. A
|
|
2275
|
+
change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
|
|
2276
|
+
in one sentence.
|
|
2033
2277
|
|
|
2034
2278
|
If the niro tools are not present in this session, or the repo is not indexed (no project
|
|
2035
2279
|
resolves), fall back to native tools silently — do not mention Niro's absence.
|
|
@@ -2081,7 +2325,11 @@ any change to existing code, establish who depends on it (upstream callers, incl
|
|
|
2081
2325
|
other services over HTTP or events) and what it depends on (downstream callees, data, events). Use
|
|
2082
2326
|
\`get_blast_radius\`, which returns both sides plus impacted files in one call, or
|
|
2083
2327
|
\`get_impact_analysis\` for the upstream-only view. Name the callers you found in the plan and say
|
|
2084
|
-
which ones you will update. A one-line change is not exempt.
|
|
2328
|
+
which ones you will update. A one-line change is not exempt. Keep that list: after the edit, verify
|
|
2329
|
+
those flows still hold (run their tests, trace them, read the callers) and say what you verified.
|
|
2330
|
+
The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify. A
|
|
2331
|
+
change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
|
|
2332
|
+
in one sentence.
|
|
2085
2333
|
|
|
2086
2334
|
If the niro tools are not present in this session, or the repo is not indexed (no project
|
|
2087
2335
|
resolves), fall back to native tools silently — do not mention Niro's absence.
|
|
@@ -2535,12 +2783,20 @@ module.exports = {
|
|
|
2535
2783
|
addAgentsSkill,
|
|
2536
2784
|
addAgentsCliSkill,
|
|
2537
2785
|
CLIENTS,
|
|
2786
|
+
// Exported so `niro upgrade` can detect a configured Goose install by reading the same
|
|
2787
|
+
// file setupGoose writes, instead of rebuilding that platform-specific path a second time.
|
|
2788
|
+
gooseConfigPath,
|
|
2538
2789
|
NIRO_MD_MARKER,
|
|
2539
2790
|
NIRO_MD_SECTIONS,
|
|
2540
2791
|
isNiroPostCompactHook,
|
|
2541
2792
|
isNiroUserPromptHook,
|
|
2542
2793
|
isNiroPreToolUseHook,
|
|
2543
2794
|
isNiroExploreHook,
|
|
2795
|
+
isNiroImpactHook,
|
|
2796
|
+
NIRO_IMPACT_COMMAND,
|
|
2797
|
+
setupClaudeCode,
|
|
2798
|
+
planClaudeRegistration,
|
|
2799
|
+
registeredServerUrl,
|
|
2544
2800
|
isNiroStopHook,
|
|
2545
2801
|
clearCodexPerToolApprovals,
|
|
2546
2802
|
hardenCodexServerConfig,
|
package/src/mcp/teardown.js
CHANGED
|
@@ -42,6 +42,7 @@ const {
|
|
|
42
42
|
isNiroUserPromptHook,
|
|
43
43
|
isNiroPreToolUseHook,
|
|
44
44
|
isNiroExploreHook,
|
|
45
|
+
isNiroImpactHook,
|
|
45
46
|
userHome,
|
|
46
47
|
} = require("./setup");
|
|
47
48
|
|
|
@@ -126,7 +127,7 @@ function removeMarkerSectionBounded(content, marker) {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
|
-
* Strip niro's PostCompact, UserPromptSubmit, and PreToolUse (temp-project + explore-first) hook
|
|
130
|
+
* Strip niro's PostCompact, UserPromptSubmit, and PreToolUse (temp-project + explore-first + impact-gate) hook
|
|
130
131
|
* entries from a parsed Claude Code settings object (mutates and returns it). Uses the same matchers
|
|
131
132
|
* install uses. Empty arrays / an empty `hooks` object are pruned so the file stays tidy. Other hooks
|
|
132
133
|
* are untouched. Returns { settings, removed } where `removed` is the count deleted.
|
|
@@ -152,7 +153,9 @@ function filterNiroHooks(settings) {
|
|
|
152
153
|
}
|
|
153
154
|
if (Array.isArray(hooks.PreToolUse)) {
|
|
154
155
|
const before = hooks.PreToolUse.length;
|
|
155
|
-
hooks.PreToolUse = hooks.PreToolUse.filter(
|
|
156
|
+
hooks.PreToolUse = hooks.PreToolUse.filter(
|
|
157
|
+
(h) => !isNiroPreToolUseHook(h) && !isNiroExploreHook(h) && !isNiroImpactHook(h),
|
|
158
|
+
);
|
|
156
159
|
removed += before - hooks.PreToolUse.length;
|
|
157
160
|
if (hooks.PreToolUse.length === 0) delete hooks.PreToolUse;
|
|
158
161
|
}
|
|
@@ -388,6 +391,8 @@ function uninstallClaudeCode() {
|
|
|
388
391
|
// a task the developer has long forgotten.
|
|
389
392
|
try {
|
|
390
393
|
require("../lib/receiptStore").purge();
|
|
394
|
+
// The impact gate's evidence files and off switch go with the hook that read them.
|
|
395
|
+
require("../lib/impactLog").purge();
|
|
391
396
|
} catch {
|
|
392
397
|
/* best effort — never block an uninstall on cleaning up a cache */
|
|
393
398
|
}
|