@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 +2 -2
- package/docs/working-on-branches.md +5 -4
- package/package.json +1 -1
- package/src/commands/init.js +2 -2
- package/src/commands/takeover.js +1 -1
- package/src/index.js +1 -1
- package/src/mcp/setup.js +99 -2
- package/src/mcp/teardown.js +8 -6
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).
|
|
27
|
-
|
|
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
|
-
-
|
|
45
|
-
-
|
|
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
|
-
|
|
76
|
-
No.
|
|
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
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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("(
|
|
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(
|
package/src/commands/takeover.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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 —
|
|
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,
|
package/src/mcp/teardown.js
CHANGED
|
@@ -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
|
|
126
|
-
* settings object (mutates and returns it). Uses the same matchers
|
|
127
|
-
* arrays / an empty `hooks` object are pruned so the file stays tidy. Other hooks
|
|
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
|
}
|