@niroai/niro 0.3.4 → 0.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niroai/niro",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Niro — one package: the niro CLI plus the Niro MCP server (code intelligence for AI coding assistants).",
5
5
  "keywords": [
6
6
  "niro",
package/src/mcp/setup.js CHANGED
@@ -116,6 +116,18 @@ function isNiroPreToolUseHook(h) {
116
116
  return cmds.some((c) => c.includes("should_create_temp_project") || c.includes("should_sandbox_repo"));
117
117
  }
118
118
 
119
+ /**
120
+ * True if `h` is niro's PreToolUse explore-first reminder entry (matcher Grep|Glob|Read). Keyed on the
121
+ * unique `niro-explore-` marker-file prefix so a foreign PreToolUse hook on the same matcher is never
122
+ * touched. Shared by installer (upgrade-in-place) and uninstaller (removal).
123
+ */
124
+ function isNiroExploreHook(h) {
125
+ const cmds = [];
126
+ if (h && Array.isArray(h.hooks)) h.hooks.forEach((i) => i && typeof i.command === "string" && cmds.push(i.command));
127
+ if (h && typeof h.command === "string") cmds.push(h.command);
128
+ return cmds.some((c) => c.includes("niro-explore-"));
129
+ }
130
+
119
131
  /**
120
132
  * Writes a niro-managed skill/rule file, refreshing it when the generated content has
121
133
  * changed. These files are GENERATED (not hand-edited), so refresh-on-change is correct:
@@ -541,6 +553,7 @@ async function setupClaudeCode(apiKey, mcpServerUrl) {
541
553
  addClaudeCodePostCompactHook();
542
554
  addClaudeCodeUserPromptHook();
543
555
  addClaudeCodePreToolUseHook();
556
+ addClaudeCodeExploreHook();
544
557
  addClaudeCodeMemory();
545
558
  addClaudeCodeSkill();
546
559
  addClaudeCodeCliSkill();
@@ -777,6 +790,88 @@ function addClaudeCodePreToolUseHook() {
777
790
  }
778
791
  }
779
792
 
793
+ /**
794
+ * The niro PreToolUse hook command (matcher Grep|Glob|Read). Prose instructions (CLAUDE.md, the
795
+ * UserPromptSubmit hook, the niro skill) tell Claude to prefer niro tools for code exploration, but in
796
+ * practice that only sticks when a task is phrased as a discovery question — a task phrased as "fix X" or
797
+ * "add Y" skips straight to native search with no interruption. This hook is the deterministic backstop:
798
+ * once per (session, repo, branch), the first native Grep/Glob/Read call in a niro-indexed repo gets an
799
+ * advisory reminder to prefer niro's structural tools instead — regardless of how the task was phrased.
800
+ *
801
+ * Dependency-free (no jq): reads the PreToolUse JSON on stdin. Read supplies `file_path`; Grep/Glob supply
802
+ * `path` (often omitted, meaning cwd) — so this tries file_path, then path, then the hook's own `cwd`
803
+ * field, then falls back to $PWD. Walks up for a `.niro-project` marker (silent outside indexed repos)
804
+ * and dedupes via a marker file keyed by session_id + repo root + branch, same scheme as the Edit|Write
805
+ * temp-project hook but under the distinct `niro-explore-` prefix so the two hooks never collide.
806
+ */
807
+ const NIRO_EXPLORE_COMMAND = [
808
+ 'IN="$(cat)"',
809
+ // Read gives file_path; Grep/Glob give path (may be absent); fall back to cwd, then $PWD.
810
+ `f="$(printf '%s' "$IN" | grep -oE '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
811
+ `[ -z "$f" ] && f="$(printf '%s' "$IN" | grep -oE '"path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
812
+ `[ -z "$f" ] && f="$(printf '%s' "$IN" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
813
+ '[ -z "$f" ] && f="$PWD"',
814
+ 'd0="$f"; [ -d "$d0" ] || d0="$(dirname "$f")"',
815
+ 'r="$(git -C "$d0" rev-parse --show-toplevel 2>/dev/null)"',
816
+ '[ -z "$r" ] && exit 0',
817
+ // only remind inside a niro-indexed tree
818
+ 'd="$r"; ok=0; while [ -n "$d" ] && [ "$d" != "/" ]; do if [ -f "$d/.niro-project" ]; then ok=1; break; fi; d="$(dirname "$d")"; done',
819
+ '[ "$ok" = 0 ] && exit 0',
820
+ `s="$(printf '%s' "$IN" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
821
+ // Branch-aware dedup, same rationale as the Edit|Write hook: a stale answer from a different branch must not
822
+ // silence the reminder after a branch switch.
823
+ 'b="$(git -C "$r" rev-parse --abbrev-ref HEAD 2>/dev/null)"',
824
+ `k="$(printf '%s\\n%s\\n%s' "$s" "$r" "$b" | cksum | cut -d' ' -f1)"`,
825
+ 'm="${TMPDIR:-/tmp}/niro-explore-$k"',
826
+ '[ -f "$m" ] && exit 0',
827
+ 'touch "$m"',
828
+ // Advisory context injection ONLY — no permissionDecision, so it never blocks the read/search.
829
+ `printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"This is a niro-indexed repo. For code structure/behavior/call-chain/impact/reuse questions - including ones that come up mid-task while implementing or fixing something, not just standalone discovery questions - prefer niro MCP tools (get_source_code, code_grep, get_call_chain, get_blast_radius, get_impact_analysis, find_reusable_code, get_class_structure, etc.) over native Grep/Glob/Read. Fall back to native tools silently for out-of-scope questions (config files, docs, runtime state, non-code content). Shown once per repo per branch per session."}}'`,
830
+ ].join('; ');
831
+
832
+ /**
833
+ * Adds the PreToolUse (Grep|Glob|Read) explore-first reminder hook to ~/.claude/settings.json. Idempotent:
834
+ * upgrades a stale niro entry in place, never touches a foreign PreToolUse hook on the same matcher
835
+ * (matched by the niro-explore- marker via isNiroExploreHook).
836
+ */
837
+ function addClaudeCodeExploreHook() {
838
+ const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
839
+ let settings = {};
840
+ try {
841
+ if (fs.existsSync(settingsPath)) settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
842
+ } catch {
843
+ // Corrupt or missing — start fresh
844
+ }
845
+
846
+ if (!settings.hooks) settings.hooks = {};
847
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
848
+
849
+ const entry = { matcher: "Grep|Glob|Read", hooks: [{ type: "command", command: NIRO_EXPLORE_COMMAND }] };
850
+ const alreadyCurrent = settings.hooks.PreToolUse.some(
851
+ (h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === NIRO_EXPLORE_COMMAND),
852
+ );
853
+ if (alreadyCurrent) {
854
+ console.log(" PreToolUse explore-first hook already up to date — skipping.\n");
855
+ return;
856
+ }
857
+
858
+ const hadStale = settings.hooks.PreToolUse.some(isNiroExploreHook);
859
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !isNiroExploreHook(h));
860
+ settings.hooks.PreToolUse.push(entry);
861
+
862
+ try {
863
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
864
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
865
+ console.log(` PreToolUse explore-first hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
866
+ console.log(" → Before the first native Grep/Glob/Read in a niro-indexed repo, Claude is reminded once/session/branch");
867
+ console.log(" to prefer niro tools — even mid-task, not just for standalone discovery questions.\n");
868
+ } catch (err) {
869
+ console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
870
+ console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
871
+ console.warn(JSON.stringify(entry, null, 2) + "\n");
872
+ }
873
+ }
874
+
780
875
  /** Body of the `niro` skill (long-form tool-routing playbook). */
781
876
  const NIRO_SKILL = `---
782
877
  name: niro-skill
@@ -908,6 +1003,7 @@ temporary index of your branch (\`niro new-temp-project\` to create, \`niro disc
908
1003
  | Full path from A to B (prefer \`to_service\` for cross-service) | \`trace_execution_path\` |
909
1004
  | HTTP endpoints a service exposes | \`find_api_endpoints\` |
910
1005
  | Outbound HTTP calls a service makes | \`find_outbound_http_calls\` |
1006
+ | Service-to-service call map (who calls whom), as a Mermaid diagram | \`get_service_topology\` |
911
1007
  | What handles an outgoing HTTP call / event | \`find_handler\` |
912
1008
 
913
1009
  **Change safely**
@@ -1207,7 +1303,7 @@ Routing (most specific wins):
1207
1303
  <!-- how_does_x_work temporarily disabled (hidden MCP tool) — re-add "how_does_x_work (forward flow) | " to the understand line below when re-enabled -->
1208
1304
  - understand: explain_code (AI docs) | get_source_code | get_class_structure
1209
1305
  - navigate: get_call_chain (callees) | get_reverse_call_chain (callers) | trace_execution_path (A to B)
1210
- - HTTP: find_api_endpoints | find_outbound_http_calls | find_handler
1306
+ - HTTP: find_api_endpoints | find_outbound_http_calls | find_handler | get_service_topology (service-to-service map, Mermaid)
1211
1307
  <!-- why_could_x_happen temporarily disabled (hidden MCP tool) — re-add "why_could_x_happen | " to the debug line below when re-enabled -->
1212
1308
  - debug: find_known_issues | get_state_at_entry
1213
1309
  - data & events: get_data_resource_usage | get_event_producers_and_handlers
@@ -1957,6 +2053,7 @@ module.exports = {
1957
2053
  isNiroPostCompactHook,
1958
2054
  isNiroUserPromptHook,
1959
2055
  isNiroPreToolUseHook,
2056
+ isNiroExploreHook,
1960
2057
  userHome,
1961
2058
  // Exposed for content regression tests (e.g. the cross-project routing guidance).
1962
2059
  NIRO_SKILL,
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * `niro mcp uninstall` removes everything `niro mcp install` wrote, per editor:
7
7
  * - the MCP server registration (claude/codex CLI, or the editor's JSON config)
8
- * - the niro hooks in ~/.claude/settings.json (PostCompact + UserPromptSubmit + PreToolUse)
8
+ * - the niro hooks in ~/.claude/settings.json (PostCompact + UserPromptSubmit + PreToolUse: both the
9
+ * Edit|Write temp-project reminder and the Grep|Glob|Read explore-first reminder)
9
10
  * - the niro section in markdown config (CLAUDE.md / AGENTS.md / Windsurf rules)
10
11
  * - the generated skills (~/.claude/skills, the shared ~/.agents/skills)
11
12
  * - Cursor's "niro:*" allowlist entry and project rule
@@ -38,6 +39,7 @@ const {
38
39
  isNiroPostCompactHook,
39
40
  isNiroUserPromptHook,
40
41
  isNiroPreToolUseHook,
42
+ isNiroExploreHook,
41
43
  userHome,
42
44
  } = require("./setup");
43
45
 
@@ -122,10 +124,10 @@ function removeMarkerSectionBounded(content, marker) {
122
124
  }
123
125
 
124
126
  /**
125
- * Strip niro's PostCompact and UserPromptSubmit hook entries from a parsed Claude Code
126
- * settings object (mutates and returns it). Uses the same matchers install uses. Empty
127
- * arrays / an empty `hooks` object are pruned so the file stays tidy. Other hooks are
128
- * untouched. Returns { settings, removed } where `removed` is the count deleted.
127
+ * Strip niro's PostCompact, UserPromptSubmit, and PreToolUse (temp-project + explore-first) hook
128
+ * entries from a parsed Claude Code settings object (mutates and returns it). Uses the same matchers
129
+ * install uses. Empty arrays / an empty `hooks` object are pruned so the file stays tidy. Other hooks
130
+ * are untouched. Returns { settings, removed } where `removed` is the count deleted.
129
131
  */
130
132
  function filterNiroHooks(settings) {
131
133
  let removed = 0;
@@ -148,7 +150,7 @@ function filterNiroHooks(settings) {
148
150
  }
149
151
  if (Array.isArray(hooks.PreToolUse)) {
150
152
  const before = hooks.PreToolUse.length;
151
- hooks.PreToolUse = hooks.PreToolUse.filter((h) => !isNiroPreToolUseHook(h));
153
+ hooks.PreToolUse = hooks.PreToolUse.filter((h) => !isNiroPreToolUseHook(h) && !isNiroExploreHook(h));
152
154
  removed += before - hooks.PreToolUse.length;
153
155
  if (hooks.PreToolUse.length === 0) delete hooks.PreToolUse;
154
156
  }