@niroai/niro 0.3.3 → 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/README.md CHANGED
@@ -23,8 +23,8 @@ niro new-temp-project # run once in the repo folder
23
23
  ```
24
24
 
25
25
  This creates your own private copy of the project that follows your branch and your
26
- uncommitted edits, live (keep `niro watch` running). Teammates are not affected and cannot
27
- see your copy. Switching branches needs nothing: Niro answers from your copy on the new
26
+ uncommitted edits, live (keep `niro watch` running). Your original project is not changed.
27
+ Switching branches needs nothing: Niro answers from your copy on the new
28
28
  branch, and from the matching indexed project on branches you already cover. When your branch
29
29
  is merged:
30
30
 
@@ -41,8 +41,8 @@ It creates your own private copy of the project inside Niro.
41
41
 
42
42
  - The copy follows your branch, including changes you have not committed.
43
43
  - Every save updates the copy, as long as "niro watch" is running.
44
- - Teammates are not affected. They keep using the shared project.
45
- - Nobody can see your copy. Not even teammates on your account.
44
+ - Your original project is not changed. It keeps its own index.
45
+ - The copy is fully separate and disposable.
46
46
 
47
47
  From then on, answers come from YOUR code.
48
48
 
@@ -72,8 +72,9 @@ of your changes together.
72
72
 
73
73
  ## Common questions
74
74
 
75
- Can teammates see my unfinished code?
76
- No. Your copy is private to you.
75
+ Does this change my original project?
76
+ No. The original project and its index stay exactly as they were.
77
+ The copy is separate; deleting it later loses nothing.
77
78
 
78
79
  Does this work with my AI assistant and my git host?
79
80
  Yes. Any assistant that supports MCP (Claude Code, Cursor, Windsurf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niroai/niro",
3
- "version": "0.3.3",
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",
@@ -171,7 +171,7 @@ async function init(pathArg, opts = {}) {
171
171
  }
172
172
  } else if (otherBranchOnly.length) {
173
173
  // Known repo, but you're on a branch Niro hasn't indexed → the honest answer is "sandbox it",
174
- // not "already indexed". A sandbox indexes your branch privately (teammates unaffected).
174
+ // not "already indexed". A sandbox indexes your branch in a separate copy (original untouched).
175
175
  const where = otherBranchOnly
176
176
  .map((p) => `"${p.alias || p.project_id}"${p.indexed_branch ? ` (indexed on ${color.cyan(p.indexed_branch)})` : ""}`)
177
177
  .join(", ");
@@ -179,7 +179,7 @@ async function init(pathArg, opts = {}) {
179
179
  console.log(`\n${color.green("✓")} This repo is part of: ${where}.`);
180
180
  console.log(` But ${yourBranch} isn't indexed by Niro — answers would reflect the indexed branch, not your local work.`);
181
181
  console.log(` ${color.cyan("Run `niro sandbox` here")} to index your branch privately ` +
182
- color.dim("(teammates unaffected; `niro discard` to undo)."));
182
+ color.dim("(original project unchanged; `niro discard` to undo)."));
183
183
  if (!opts.addTo) {
184
184
  if (prompt.isNonInteractive()) {
185
185
  console.log(color.dim(
@@ -72,7 +72,7 @@ async function takeover(pathArg, opts = {}) {
72
72
  }
73
73
 
74
74
  console.log(`\n Creating a temporary private project of ${color.bold(source.alias || source.project_id)}...`);
75
- console.log(color.dim(" Your teammates' view of the project is not affected."));
75
+ console.log(color.dim(" Your original project is not changed."));
76
76
 
77
77
  // 2. Create-or-extend server-side (ADR-016). If you already have a temporary project of this project,
78
78
  // the backend EXTENDS it — adds this repo to the same copy — instead of minting a second one, so
package/src/index.js CHANGED
@@ -157,7 +157,7 @@ function main() {
157
157
  program
158
158
  .command("new-temp-project [path]")
159
159
  .aliases(["sandbox", "takeover"])
160
- .description("Create a temporary private project for this folder that reflects your branch + local edits, so Niro's answers match your work (teammates unaffected). Remove it with `niro discard-temp-project`.")
160
+ .description("Create a temporary private project for this folder that reflects your branch + local edits, so Niro's answers match your work (your original project is unchanged). Remove it with `niro discard-temp-project`.")
161
161
  .option("--project <id_or_alias>", "which project to base the temporary project on when the repo maps to several")
162
162
  .action((pathArg, opts) => run(async () => {
163
163
  const cmd = require("./commands/takeover");
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
@@ -840,7 +935,7 @@ lost"). When the target of the question is a repo other than the current one:
840
935
 
841
936
  Niro indexes a repo at ONE branch (the team's tracked branch). Identity is (git url, branch): a graph
842
937
  built on \`main\` does NOT reflect your feature branch or uncommitted edits. A **temporary project** is a private,
843
- temporary index of your branch (\`niro new-temp-project\` to create, \`niro discard-temp-project\` to remove — teammates unaffected).
938
+ temporary index of your branch (\`niro new-temp-project\` to create, \`niro discard-temp-project\` to remove — the original project is unchanged).
844
939
 
845
940
  - **Obey \`🔔 Niro ACTION\` notices.** When a tool result appends "the coding assistant should run
846
941
  \`niro new-temp-project\` / \`niro discard-temp-project\`", run that command with the Bash tool (cwd = that
@@ -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
  }