@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.
Files changed (177) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +123 -302
  3. package/bin/niro.js +12 -1
  4. package/docs/agent-antigravity.md +37 -0
  5. package/docs/agent-claude-code.md +48 -0
  6. package/docs/agent-claude-desktop.md +47 -0
  7. package/docs/agent-cline.md +42 -0
  8. package/docs/agent-codex.md +45 -0
  9. package/docs/agent-copilot-cli.md +42 -0
  10. package/docs/agent-cursor.md +44 -0
  11. package/docs/agent-factory.md +42 -0
  12. package/docs/agent-gemini-cli.md +42 -0
  13. package/docs/agent-goose.md +42 -0
  14. package/docs/agent-kiro.md +37 -0
  15. package/docs/agent-opencode.md +37 -0
  16. package/docs/agent-vscode.md +45 -0
  17. package/docs/agent-windsurf.md +38 -0
  18. package/docs/answer-shapes.md +69 -0
  19. package/docs/builds-and-updates.md +68 -0
  20. package/docs/cloud-README.md +372 -0
  21. package/docs/configuring-a-folder.md +51 -0
  22. package/docs/connect-ci.md +24 -0
  23. package/docs/connect-overview.md +20 -0
  24. package/docs/connect-review-bots.md +17 -0
  25. package/docs/connect-team-server.md +12 -0
  26. package/docs/connect-terminal.md +49 -0
  27. package/docs/connect-web-agents.md +9 -0
  28. package/docs/connect-your-ai-assistant.md +59 -175
  29. package/docs/coverage-audit-and-consent.md +51 -0
  30. package/docs/evaluate-what-niro-did.md +271 -0
  31. package/docs/faq-assistants-and-mcp.md +122 -0
  32. package/docs/faq-feedback-and-receipts.md +33 -0
  33. package/docs/faq-indexing.md +127 -0
  34. package/docs/faq-install-and-setup.md +103 -0
  35. package/docs/faq-licence-and-plans.md +86 -0
  36. package/docs/faq-login-and-account.md +69 -0
  37. package/docs/faq-offline.md +34 -0
  38. package/docs/faq-performance.md +33 -0
  39. package/docs/faq-privacy-and-security.md +113 -0
  40. package/docs/faq-uninstall.md +47 -0
  41. package/docs/faq-updates.md +59 -0
  42. package/docs/faq-worktrees-and-branches.md +59 -0
  43. package/docs/faq-wrong-or-missing-answers.md +275 -0
  44. package/docs/hosted-only.md +33 -0
  45. package/docs/how-the-graph-works.md +47 -0
  46. package/docs/index.md +157 -0
  47. package/docs/licence-and-plans.md +72 -0
  48. package/docs/lifecycle-debug.md +48 -0
  49. package/docs/lifecycle-onboarding.md +32 -0
  50. package/docs/lifecycle-overview.md +77 -0
  51. package/docs/lifecycle-plan-and-design.md +44 -0
  52. package/docs/lifecycle-review.md +30 -0
  53. package/docs/lifecycle-security-and-privacy.md +35 -0
  54. package/docs/lifecycle-test.md +28 -0
  55. package/docs/lifecycle-write-code.md +35 -0
  56. package/docs/llms-full.txt +8015 -0
  57. package/docs/llms.txt +126 -0
  58. package/docs/playbook-add-a-feature.md +108 -0
  59. package/docs/playbook-change-safely.md +93 -0
  60. package/docs/playbook-data-and-events.md +107 -0
  61. package/docs/playbook-debug-after-a-change.md +96 -0
  62. package/docs/playbook-debug-config.md +95 -0
  63. package/docs/playbook-debug-exception.md +124 -0
  64. package/docs/playbook-debug-failing-service-call.md +119 -0
  65. package/docs/playbook-debug-slow-path.md +80 -0
  66. package/docs/playbook-debug-wrong-value.md +137 -0
  67. package/docs/playbook-onboard.md +95 -0
  68. package/docs/playbook-review-a-change.md +72 -0
  69. package/docs/playbook-test-a-change.md +73 -0
  70. package/docs/reference-cli.md +113 -0
  71. package/docs/reference-config-files.md +42 -0
  72. package/docs/reference-engine-commands.md +56 -0
  73. package/docs/reference-environment-variables.md +89 -0
  74. package/docs/reference-exit-codes.md +67 -0
  75. package/docs/reference-limits.md +75 -0
  76. package/docs/reference-mcp-tools.md +326 -0
  77. package/docs/reference-messages.md +180 -0
  78. package/docs/secrets-and-masking.md +48 -0
  79. package/docs/supported-c.md +130 -0
  80. package/docs/supported-cpp.md +157 -0
  81. package/docs/supported-csharp.md +211 -0
  82. package/docs/supported-go.md +258 -0
  83. package/docs/supported-java.md +226 -0
  84. package/docs/supported-javascript.md +223 -0
  85. package/docs/supported-kotlin.md +124 -0
  86. package/docs/supported-overview.md +89 -0
  87. package/docs/supported-php.md +157 -0
  88. package/docs/supported-python.md +224 -0
  89. package/docs/supported-ruby.md +162 -0
  90. package/docs/supported-rust.md +150 -0
  91. package/docs/supported-swift.md +133 -0
  92. package/docs/supported-typescript.md +220 -0
  93. package/docs/tokens-speed-and-accuracy.md +99 -0
  94. package/docs/troubleshooting.md +42 -0
  95. package/docs/uninstall.md +74 -0
  96. package/docs/what-leaves-your-machine.md +71 -0
  97. package/docs/where-things-are-stored.md +59 -0
  98. package/docs/worktrees-and-branches.md +55 -0
  99. package/package.json +5 -3
  100. package/src/commands/add-repo.js +5 -0
  101. package/src/commands/auditSend.js +67 -0
  102. package/src/commands/build.js +6 -0
  103. package/src/commands/docsSend.js +73 -0
  104. package/src/commands/engine.js +120 -0
  105. package/src/commands/engineRoutes.js +105 -0
  106. package/src/commands/guide.js +254 -0
  107. package/src/commands/init.js +16 -7
  108. package/src/commands/initLocal.js +96 -0
  109. package/src/commands/journalSend.js +47 -0
  110. package/src/commands/login.js +89 -12
  111. package/src/commands/mcp.js +22 -6
  112. package/src/commands/plan.js +99 -0
  113. package/src/commands/repoPush.js +33 -0
  114. package/src/commands/requestIntegration.js +38 -0
  115. package/src/commands/share.js +56 -0
  116. package/src/commands/start.js +451 -371
  117. package/src/commands/status.js +57 -0
  118. package/src/commands/takeover.js +6 -0
  119. package/src/commands/uninstall.js +180 -0
  120. package/src/commands/upgrade.js +38 -8
  121. package/src/commands/watch.js +420 -6
  122. package/src/index.js +174 -3
  123. package/src/lib/agentApi.js +32 -1
  124. package/src/lib/api.js +20 -6
  125. package/src/lib/auditUpload.js +293 -0
  126. package/src/lib/branchDrift.js +149 -0
  127. package/src/lib/choose.js +116 -0
  128. package/src/lib/cloudGuard.js +74 -0
  129. package/src/lib/credentials.js +2 -2
  130. package/src/lib/docsMarked.js +55 -0
  131. package/src/lib/docsState.js +213 -0
  132. package/src/lib/docsUpload.js +481 -0
  133. package/src/lib/engine.js +619 -0
  134. package/src/lib/folderSync.js +2 -1
  135. package/src/lib/graphCheck.js +40 -0
  136. package/src/lib/guide.js +293 -0
  137. package/src/lib/guideSearch.js +252 -0
  138. package/src/lib/integrationRequest.js +89 -0
  139. package/src/lib/journalUpload.js +147 -0
  140. package/src/lib/languages.js +129 -0
  141. package/src/lib/lease.js +375 -0
  142. package/src/lib/localBuild.js +285 -0
  143. package/src/lib/localEngine.js +210 -0
  144. package/src/lib/localOnboard.js +124 -0
  145. package/src/lib/localWorktrees.js +243 -0
  146. package/src/lib/machineCheck.js +58 -0
  147. package/src/lib/mcpClients.js +10 -1
  148. package/src/lib/migrationNotice.js +118 -0
  149. package/src/lib/modelFetch.js +153 -0
  150. package/src/lib/nestedOutput.js +23 -9
  151. package/src/lib/onboardingLog.js +34 -0
  152. package/src/lib/planUsage.js +165 -0
  153. package/src/lib/progressBar.js +46 -0
  154. package/src/lib/prompt.js +1 -1
  155. package/src/lib/redact.js +155 -0
  156. package/src/lib/release.js +33 -0
  157. package/src/lib/repoPush.js +495 -0
  158. package/src/lib/secret.js +1 -1
  159. package/src/lib/service.js +372 -63
  160. package/src/lib/shareCode.js +367 -0
  161. package/src/lib/shareExclusions.js +114 -0
  162. package/src/lib/shareFiles.js +129 -0
  163. package/src/lib/spinner.js +9 -5
  164. package/src/lib/watchDaemon.js +33 -10
  165. package/src/lib/watchState.js +12 -1
  166. package/src/lib/windowsPaths.js +67 -0
  167. package/src/mcp/lineCheck.js +382 -0
  168. package/src/mcp/lineName.js +92 -0
  169. package/src/mcp/localBackend.js +751 -0
  170. package/src/mcp/localMode.js +383 -0
  171. package/src/mcp/localSkill.js +189 -0
  172. package/src/mcp/server.js +610 -79
  173. package/src/mcp/setup.js +519 -131
  174. package/src/mcp/shellSearchHook.js +509 -0
  175. package/src/mcp/teardown.js +173 -91
  176. package/src/mcp/usingNiro.js +39 -0
  177. package/docs/working-on-branches.md +0 -162
@@ -0,0 +1,509 @@
1
+ /**
2
+ * The PreToolUse reminder on Claude Code's Bash tool: when a shell command searches or reads source
3
+ * code in an indexed folder, it adds the same "ask niro first" note that the Grep|Glob|Read hook adds
4
+ * (setup.js NIRO_EXPLORE_COMMAND). It never blocks the command: there is no permissionDecision.
5
+ *
6
+ * Why it exists. Agents mostly search with the shell, not with Claude Code's own search tools, so the
7
+ * Grep|Glob|Read reminder never fired for them. Ten days of our own sessions held 10,347 shell greps and
8
+ * reads of source code against 2,112 niro questions (dev-process review, 2026-11-10, proposal P8).
9
+ * Niraj approved it on 2026-11-10: "proposals 1 and 8 yes but we should first test them".
10
+ *
11
+ * How it decides, in one awk pass over the hook input (no jq, no node, no network):
12
+ * 1. Read `tool_input.command` and the session `cwd` out of the JSON.
13
+ * 2. Split the command the way a shell does: quotes, `&&`, `||`, `;`, `|`, `$(`, newlines; heredoc
14
+ * bodies are skipped; a leading `cd` moves the folder later parts run in.
15
+ * 3. For each simple command, name the paths it would search or read:
16
+ * - `grep egrep fgrep rg ag ack`, `git grep`: the paths given, or the current folder when the tool
17
+ * searches it by default (rg, ag, ack, git grep, grep -r). A tool reading a pipe searches no file.
18
+ * `git grep pattern <revision> -- path` searches another commit, which niro cannot answer: quiet.
19
+ * - `cat head tail sed -n awk less more nl bat`: files with a source extension (or a glob of them).
20
+ * - `find ... -name/-iname/-path/-regex` with a source extension.
21
+ * - `xargs grep ...`: the current folder.
22
+ * 4. Leave out config, docs, logs and generated output: a file whose extension is not source, a path
23
+ * through docs/, logs/, node_modules/, dist/, build/, target/ and the like, a search limited to
24
+ * non-source files (`--include=*.md`, `rg -t md`), `tail -f`, `sed -i`, `--help` and `--version`.
25
+ * build/, dist/, target/, out/ and logs/ count only as the first folder of the path the command names.
26
+ * A variable set earlier in the same command (`f=src/a.py; sed -n 1,9p $f`, `export R=...`) is filled in;
27
+ * one set from a command's output (`f=$(find ...)`) is not known, and a path that uses it is skipped.
28
+ * The shell then keeps a folder only if it exists, and a file only if it exists and was not changed in
29
+ * the last 10 minutes (a file just edited is being checked, not explored). On the first path that
30
+ * passes, it walks up for the indexed-folder marker as the explore hook does (localMode FAST_CLOUD_WALK),
31
+ * so localHook turns it into the niro-local check without any change here.
32
+ *
33
+ * `niro-shell-search-` is this hook's identity token: the installer and the uninstaller find it by
34
+ * that string. It must never contain `niro-explore-`, `niro-impact-` or `should_create_temp_project`,
35
+ * or another niro installer would take it for its own hook and delete it (checked when this module loads).
36
+ */
37
+ const fs = require("fs");
38
+ const path = require("path");
39
+ const { FAST_CLOUD_WALK } = require("./localMode");
40
+
41
+ /** Source file extensions. The impact hook's list (setup.js NIRO_IMPACT_COMMAND) plus a few more languages. */
42
+ const SOURCE_EXTENSIONS = [
43
+ "java", "js", "mjs", "cjs", "ts", "tsx", "jsx", "mts", "cts", "py", "pyi", "go", "rb", "kt", "kts", "cs",
44
+ "php", "rs", "scala", "swift", "c", "cc", "cpp", "cxx", "h", "hh", "hpp", "m", "mm", "dart", "lua", "ex",
45
+ "exs", "clj", "groovy", "vue", "svelte", "erl", "hs", "ml", "fs", "vb",
46
+ ];
47
+
48
+ /** ripgrep `-t` names for those languages. */
49
+ const SOURCE_TYPES = [
50
+ "py", "python", "js", "javascript", "ts", "typescript", "jsx", "tsx", "java", "go", "rust", "rs", "c", "cpp",
51
+ "cs", "csharp", "kotlin", "kt", "ruby", "rb", "php", "swift", "scala", "lua", "dart", "elixir", "clojure",
52
+ "groovy", "objc", "objcpp", "vue", "svelte", "haskell", "erlang", "ocaml", "fsharp",
53
+ ];
54
+
55
+ /** Folder names that hold docs, dependencies or caches wherever they appear in a path. */
56
+ const EXCLUDED_FOLDERS = [
57
+ "docs", "doc", "node_modules", "coverage", ".git", "__pycache__", ".venv", "venv", "vendor", ".next",
58
+ ".nuxt", ".cache", "site-packages", ".gradle", ".pytest_cache", ".mypy_cache",
59
+ ];
60
+
61
+ /**
62
+ * Folder names that hold logs or build output only as the first folder of the path the command names
63
+ * (`grep -r x build/`, `cat dist/app.js`). Deeper down they are often source packages: niro-local's own
64
+ * `src/niro_local/build/`, a Java `com/acme/log/` package. The trial missed 3 such reads before this split.
65
+ */
66
+ const EXCLUDED_AT_TOP = ["build", "dist", "target", "out", "logs", "log", "tmp"];
67
+
68
+ /**
69
+ * The classifier. Prints one line per path the command would search or read, at most four:
70
+ * `D/abs/folder` or `F/abs/file`. Prints nothing when the command is not a code search or read.
71
+ * POSIX awk only (mawk, BSD awk, gawk): no gensub, no arrays of arrays, no match() third argument.
72
+ * A backslash in the decoded command is held as \001, so the JSON and shell escapes never meet.
73
+ * It must contain no single quote, because the shell command wraps it in single quotes.
74
+ */
75
+ const AWK_PROGRAM = String.raw`
76
+ function setof(list, arr, n, i, p) { n = split(list, p, " "); for (i = 1; i <= n; i++) arr[p[i]] = 1 }
77
+ function jstr(s, key, i, rest, t, e, b) {
78
+ rest = s
79
+ while ((i = index(rest, "\"" key "\"")) > 0) {
80
+ rest = substr(rest, i + length(key) + 2)
81
+ if (!match(rest, /^[ \t\r\n]*:[ \t\r\n]*"/)) continue
82
+ t = substr(rest, RLENGTH + 1, 16384)
83
+ gsub(/\\\\/, "\001", t)
84
+ if (substr(t, 1, 1) == "\"") return ""
85
+ if (match(t, /[^\\]"/)) t = substr(t, 1, RSTART)
86
+ gsub(/\\"/, "\"", t); gsub(/\\n/, "\n", t); gsub(/\\t/, "\t", t); gsub(/\\\//, "/", t)
87
+ gsub(/\\[rbf]/, " ", t); gsub(/\\u0027/, "\047", t); gsub(/\\u003[cC]/, "<", t); gsub(/\\u003[eE]/, ">", t)
88
+ gsub(/\\u0026/, "\\&", t); gsub(/\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]/, "?", t)
89
+ return t
90
+ }
91
+ return ""
92
+ }
93
+ function tok(ty, v) { NT++; TT[NT] = ty; TV[NT] = v }
94
+ function flush() { if (INW) tok("w", W0); W0 = ""; INW = 0 }
95
+ function skiphd(b, i, hd, rest, off, k, a, before) {
96
+ rest = substr(b, i + 1); off = 0
97
+ while ((k = index(rest, hd)) > 0) {
98
+ a = substr(rest, k + length(hd), 1)
99
+ before = substr(rest, 1, k - 1); sub(/\t*$/, "", before)
100
+ if ((a == "\n" || a == "") && (before == "" || substr(before, length(before), 1) == "\n")) return i + off + k + length(hd)
101
+ rest = substr(rest, k + 1); off += k
102
+ }
103
+ return length(b) + 1
104
+ }
105
+ function lex(b, n, i, c, d, e, j, hd, hdw) {
106
+ n = length(b); i = 1; W0 = ""; INW = 0; NT = 0; hd = ""
107
+ while (i <= n) {
108
+ c = substr(b, i, 1); d = substr(b, i + 1, 1)
109
+ if (c == "\001") { if (d == "\n") { i += 2; continue } W0 = W0 d; INW = 1; i += 2; continue }
110
+ if (c == "\047") {
111
+ j = index(substr(b, i + 1), "\047")
112
+ if (j == 0) { W0 = W0 substr(b, i + 1); i = n + 1 } else { W0 = W0 substr(b, i + 1, j - 1); i += j + 1 }
113
+ INW = 1; continue
114
+ }
115
+ if (c == "\"") {
116
+ i++
117
+ while (i <= n) {
118
+ d = substr(b, i, 1)
119
+ if (d == "\"") { i++; break }
120
+ if (d == "\001") { e = substr(b, i + 1, 1); if (e == "\"" || e == "\001" || e == "$" || e == "\140") { W0 = W0 e; i += 2; continue } }
121
+ W0 = W0 d; i++
122
+ }
123
+ INW = 1; continue
124
+ }
125
+ if (c == " " || c == "\t") { flush(); i++; continue }
126
+ if (c == "#" && !INW) { j = index(substr(b, i), "\n"); if (j == 0) i = n + 1; else i += j - 1; continue }
127
+ if (c == "\n") { flush(); tok("o", ";"); if (hd != "") { i = skiphd(b, i, hd); hd = "" } else i++; continue }
128
+ if (c == ";") { flush(); tok("o", ";"); i += (d == ";") ? 2 : 1; continue }
129
+ if (c == "&") {
130
+ flush()
131
+ if (d == "&") { tok("o", ";"); i += 2 } else if (d == ">") { tok("r", ">"); i += 2; if (substr(b, i, 1) == ">") i++ } else { tok("o", ";"); i++ }
132
+ continue
133
+ }
134
+ if (c == "|") { flush(); if (d == "|") { tok("o", ";"); i += 2 } else { tok("o", "|"); i += (d == "&") ? 2 : 1 } continue }
135
+ if (c == "(" || c == "\140") { flush(); tok("o", "("); i++; continue }
136
+ if (c == ")") { flush(); tok("o", ";"); i++; continue }
137
+ if (c == "$" && d == "(") { flush(); tok("o", "("); i += 2; continue }
138
+ if (c == "<" || c == ">") {
139
+ if (W0 ~ /^[0-9]+$/) { W0 = ""; INW = 0 }
140
+ flush()
141
+ if (c == "<" && d == "<" && substr(b, i + 2, 1) == "<") { tok("r", "<<<"); i += 3; continue }
142
+ if (c == "<" && d == "<") {
143
+ i += 2; if (substr(b, i, 1) == "-") i++
144
+ while (substr(b, i, 1) == " ") i++
145
+ hdw = ""
146
+ while (i <= n) { e = substr(b, i, 1); if (e ~ /[ \t\n;&|<>()]/) break; if (e != "\047" && e != "\"" && e != "\001") hdw = hdw e; i++ }
147
+ if (hdw != "") hd = hdw
148
+ continue
149
+ }
150
+ if (c == "<" && d == "(") { tok("o", "("); i += 2; continue }
151
+ if (c == "<") { tok("r", (d == ">") ? ">" : "<"); i += (d == ">" || d == "&") ? 2 : 1; continue }
152
+ i++; if (d == ">" || d == "|" || d == "&") i++
153
+ tok("r", ">"); continue
154
+ }
155
+ W0 = W0 c; INW = 1; i++
156
+ }
157
+ flush()
158
+ }
159
+ function base(w, k) { k = w; sub(/.*\//, "", k); return k }
160
+ function extof(name, k, r) {
161
+ if (name == "." || name == "..") return ""
162
+ r = name; if (!match(r, /\.[^.]*$/) || RSTART == 1) return ""
163
+ return tolower(substr(r, RSTART + 1))
164
+ }
165
+ function srcext(s, n, i, p, e) {
166
+ s = tolower(s); gsub(/[{},()]/, "|.", s); n = split(s, p, "|")
167
+ for (i = 1; i <= n; i++) { if (!match(p[i], /\.[^.]*$/)) continue; e = substr(p[i], RSTART + 1); gsub(/[^a-z0-9+]/, "", e); if (e in SRC) return 1 }
168
+ return 0
169
+ }
170
+ function excl(p, n, i, c, f) {
171
+ n = split(p, c, "/"); f = 0
172
+ for (i = 1; i <= n; i++) { if (c[i] in EXC) return 1; if (c[i] != "" && c[i] != "." && !f) { f = 1; if (substr(p, 1, 1) != "/" && (c[i] in TOP)) return 1 } }
173
+ return 0
174
+ }
175
+ function expand(s, out, k, nm, rest, e) {
176
+ out = ""
177
+ while ((k = index(s, "$")) > 0) {
178
+ out = out substr(s, 1, k - 1); rest = substr(s, k + 1)
179
+ if (substr(rest, 1, 1) == "{") { e = index(rest, "}"); if (e == 0) return out "$" rest; nm = substr(rest, 2, e - 2); rest = substr(rest, e + 1) }
180
+ else if (match(rest, /^[A-Za-z_][A-Za-z0-9_]*/)) { nm = substr(rest, 1, RLENGTH); rest = substr(rest, RLENGTH + 1) }
181
+ else return out "$" rest
182
+ if (nm == "HOME") out = out HOME; else if (nm == "PWD") out = out CUR; else if (nm in VARS) out = out VARS[nm]; else return out "$" "{" nm "}" rest
183
+ s = rest
184
+ }
185
+ return out s
186
+ }
187
+ function tail3(p, n, c, s, i) { n = split(p, c, "/"); s = ""; for (i = (n > 3 ? n - 2 : 1); i <= n; i++) s = s "/" c[i]; return s }
188
+ function emit(k, a) {
189
+ gsub(/\/\.\//, "/", a); gsub(/\/\.\//, "/", a); sub(/\/\.$/, "", a); gsub(/\/\/+/, "/", a); if (a == "") a = "/"
190
+ if ((k a) in SEEN || NOUT >= 4) return
191
+ SEEN[k a] = 1; NOUT++; print k a
192
+ }
193
+ function cand(p, mode, a, nc, cs, g, i, last, ext, pre, np, ps) {
194
+ if (p == "" || p == "-") return
195
+ if (p == "~") p = HOME; else if (substr(p, 1, 2) == "~/") p = HOME substr(p, 2)
196
+ p = expand(p); if (index(p, "$") > 0) return
197
+ a = (substr(p, 1, 1) == "/") ? p : CUR "/" p
198
+ if (excl(p)) return
199
+ if (substr(p, 1, 1) != "/" && excl(tail3(CUR))) return
200
+ np = split(p, ps, "/"); g = 0
201
+ for (i = 1; i <= np; i++) if (ps[i] ~ /[*?[]/) { g = i; break }
202
+ if (g > 0) {
203
+ last = ps[np]
204
+ if (index(last, ".") > 0) { if (!srcext(last)) return } else if (mode == "read") return
205
+ pre = ""; for (i = 1; i < g; i++) pre = pre (i > 1 ? "/" : "") ps[i]
206
+ if (substr(p, 1, 1) == "/") pre = (pre == "") ? "/" : pre; else pre = (pre == "") ? CUR : CUR "/" pre
207
+ emit("D", pre); return
208
+ }
209
+ nc = split(a, cs, "/")
210
+ while (nc > 1 && cs[nc] == "") nc--
211
+ ext = extof(cs[nc])
212
+ if (ext != "") { if (ext in SRC) emit("F", a); return }
213
+ if (mode == "read") return
214
+ emit("D", a)
215
+ }
216
+ function resolve(p) {
217
+ p = expand(p)
218
+ if (p == "" || p == "~") return HOME
219
+ if (substr(p, 1, 2) == "~/") return HOME substr(p, 2)
220
+ return (substr(p, 1, 1) == "/") ? p : CUR "/" p
221
+ }
222
+ function search(tool, j, m, sv, lv, pat, rec, inc, incs, files, np, P, w, name, val, e, k, ch, endo) {
223
+ m = NW
224
+ if (tool == "rg") { sv = "efABCmgtTMjdrE"; lv = " regexp file glob iglob type type-not type-add max-count max-columns max-depth max-filesize replace encoding context after-context before-context sort sortr colors pre pre-glob ignore-file threads path-separator context-separator engine " }
225
+ else if (tool == "ag") { sv = "ABCmGgp"; lv = " file-search-regex ignore ignore-dir depth context after before max-count pager path-to-ignore workers " }
226
+ else if (tool == "ack") { sv = "ABCm"; lv = " ignore-dir context after-context before-context max-count " }
227
+ else if (tool == "gitgrep") { sv = "efABCm"; lv = " max-depth threads context after-context before-context max-count " }
228
+ else { sv = "efABCmdD"; lv = " include exclude exclude-dir exclude-from regexp file after-context before-context context max-count directories devices label binary-files " }
229
+ pat = 0; rec = 0; inc = 0; incs = 0; files = 0; np = 0; endo = 0
230
+ for (; j <= m; j++) {
231
+ w = WD[j]
232
+ if (!endo && w == "--") { if (tool == "gitgrep" && np > 0) return; endo = 1; continue }
233
+ if (!endo && w ~ /^--./) {
234
+ name = substr(w, 3); val = ""
235
+ if ((e = index(name, "=")) > 0) { val = substr(name, e + 1); name = substr(name, 1, e - 1) } else if (index(lv, " " name " ") > 0) { j++; val = WD[j] }
236
+ if (name == "help" || name == "version") return
237
+ if (name == "regexp" || name == "file" || name == "file-search-regex") pat = 1
238
+ if (name == "recursive" || name == "dereference-recursive" || (name == "directories" && val == "recurse")) rec = 1
239
+ if (name == "include" || ((name == "glob" || name == "iglob") && substr(val, 1, 1) != "!")) { inc = 1; if (srcext(val)) incs = 1 }
240
+ if (name == "file-search-regex") { inc = 1; if (srcext(val)) incs = 1 }
241
+ if (name == "type") { inc = 1; if (val in STY) incs = 1 }
242
+ if (name == "files" && tool == "rg") files = 1
243
+ continue
244
+ }
245
+ if (!endo && w ~ /^-[0-9]+$/) continue
246
+ if (!endo && w ~ /^-./) {
247
+ if (tool == "rg" && w == "-V") return
248
+ for (k = 2; k <= length(w); k++) {
249
+ ch = substr(w, k, 1)
250
+ if (index(sv, ch) > 0) {
251
+ val = substr(w, k + 1); if (val == "") { j++; val = WD[j] }
252
+ if (tool != "ag" && (ch == "e" || ch == "f")) pat = 1
253
+ if (tool == "ag" && ch == "g") pat = 1
254
+ if ((tool == "rg" && ch == "g" && substr(val, 1, 1) != "!") || (tool == "ag" && ch == "G")) { inc = 1; if (srcext(val)) incs = 1 }
255
+ if (tool == "rg" && ch == "t") { inc = 1; if (val in STY) incs = 1 }
256
+ if (tool == "grep" && ch == "d" && val == "recurse") rec = 1
257
+ break
258
+ }
259
+ if (tool == "grep" && (ch == "r" || ch == "R")) rec = 1
260
+ }
261
+ continue
262
+ }
263
+ if (!pat && !files) { pat = 1; continue }
264
+ P[++np] = w
265
+ }
266
+ if ((inc || files) && !incs) return
267
+ if (np == 0) {
268
+ if (NIN > 0) { for (k = 1; k <= NIN; k++) cand(IN[k], "read"); return }
269
+ if (PIN && !XARGS) return
270
+ if (tool == "grep" && !rec && !XARGS) return
271
+ cand(".", "search"); return
272
+ }
273
+ for (k = 1; k <= np; k++) cand(P[k], "search")
274
+ }
275
+ function readcmd(tool, j, m, sv, lv, scr, nseen, inpl, foll, np, P, w, name, val, e, k, ch, endo) {
276
+ m = NW; sv = ""; lv = " "
277
+ if (tool == "head" || tool == "tail") { sv = "ncs"; lv = " lines bytes sleep-interval pid max-unchanged-stats " }
278
+ else if (tool == "sed") { sv = "efl"; lv = " expression file line-length " }
279
+ else if (tool == "awk") { sv = "fvF"; lv = " file assign field-separator " }
280
+ else if (tool == "bat") { sv = "lrHm"; lv = " language line-range highlight-line map-syntax style theme tabs wrap terminal-width pager decorations color italic-text file-name " }
281
+ else if (tool == "less") { sv = "bhjkoOpPtTxyz#" }
282
+ else if (tool == "nl") { sv = "bdfhilnsvw" }
283
+ scr = (tool == "sed" || tool == "awk") ? 0 : 1; nseen = 0; inpl = 0; foll = 0; np = 0; endo = 0
284
+ for (; j <= m; j++) {
285
+ w = WD[j]
286
+ if (!endo && w == "--") { endo = 1; continue }
287
+ if (!endo && w ~ /^--./) {
288
+ name = substr(w, 3)
289
+ if ((e = index(name, "=")) > 0) name = substr(name, 1, e - 1); else if (index(lv, " " name " ") > 0) j++
290
+ if (name == "help" || name == "version" || name == "in-place") return
291
+ if (name == "follow") foll = 1
292
+ if (name == "quiet" || name == "silent") nseen = 1
293
+ if (name == "expression" || name == "file") scr = 1
294
+ continue
295
+ }
296
+ if (!endo && w ~ /^-[0-9]+$/) continue
297
+ if (!endo && (tool == "head" || tool == "tail") && w ~ /^\+[0-9]+$/) continue
298
+ if (!endo && w ~ /^-./) {
299
+ for (k = 2; k <= length(w); k++) {
300
+ ch = substr(w, k, 1)
301
+ if (index(sv, ch) > 0) {
302
+ if (substr(w, k + 1) == "") j++
303
+ if ((tool == "sed" && (ch == "e" || ch == "f")) || (tool == "awk" && ch == "f")) scr = 1
304
+ break
305
+ }
306
+ if (tool == "sed" && ch == "n") nseen = 1
307
+ if (tool == "sed" && ch == "i") inpl = 1
308
+ if (tool == "tail" && (ch == "f" || ch == "F")) foll = 1
309
+ }
310
+ continue
311
+ }
312
+ if (!scr) { scr = 1; continue }
313
+ if (tool == "awk" && w ~ /^[A-Za-z_][A-Za-z0-9_]*=/) continue
314
+ P[++np] = w
315
+ }
316
+ if (foll || inpl || (tool == "sed" && !nseen)) return
317
+ if (np == 0) { for (k = 1; k <= NIN; k++) cand(IN[k], "read"); return }
318
+ for (k = 1; k <= np; k++) cand(P[k], "read")
319
+ }
320
+ function findcmd(j, m, np, P, k, w, src) {
321
+ m = NW; np = 0; src = 0
322
+ while (j <= m && WD[j] ~ /^-[HLP]$|^-O[0-9]$/) j++
323
+ if (j <= m && WD[j] == "-D") j += 2
324
+ while (j <= m && WD[j] !~ /^[-(!]/) { P[++np] = WD[j]; j++ }
325
+ for (k = j; k < m; k++) if (WD[k] ~ /^-(i?name|i?path|i?wholename|i?regex|lname)$/ && srcext(WD[k + 1])) src = 1
326
+ if (!src) return
327
+ if (np == 0) { cand(".", "search"); return }
328
+ for (k = 1; k <= np; k++) cand(P[k], "search")
329
+ }
330
+ function assign(w, e, nm, v) {
331
+ e = index(w, "="); nm = substr(w, 1, e - 1); v = substr(w, e + 1)
332
+ if (v == "~") v = HOME; else if (substr(v, 1, 2) == "~/") v = HOME substr(v, 2)
333
+ v = expand(v)
334
+ if (v == "" || index(v, "$") > 0) delete VARS[nm]; else VARS[nm] = v
335
+ }
336
+ function segment( i, w, b, t, j, gd) {
337
+ i = 1; XARGS = 0
338
+ for (j = 1; j <= NW && WD[j] ~ /^[A-Za-z_][A-Za-z0-9_]*=/; j++) ;
339
+ if (j > NW || WD[j] == "export" || WD[j] == "local" || WD[j] == "readonly") {
340
+ for (j = 1; j <= NW; j++) if (WD[j] ~ /^[A-Za-z_][A-Za-z0-9_]*=/) assign(WD[j])
341
+ return
342
+ }
343
+ while (i <= NW) {
344
+ w = WD[i]; b = base(w)
345
+ if (w ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { i++; continue }
346
+ if (w == "!" || w == "{" || b == "if" || b == "then" || b == "else" || b == "elif" || b == "do" || b == "while" || b == "until" || b == "time" || b == "nohup" || b == "builtin" || b == "exec" || b == "stdbuf" || b == "env") {
347
+ i++; while (i <= NW && WD[i] ~ /^-/) { if (b == "env" && WD[i] == "-u") i++; i++ } continue
348
+ }
349
+ if (b == "command") { if (WD[i + 1] ~ /^-[vV]/) return; i++; continue }
350
+ if (b == "sudo" || b == "nice" || b == "timeout" || b == "xargs") {
351
+ if (b == "xargs") XARGS = 1
352
+ i++
353
+ while (i <= NW && WD[i] ~ /^-/) { if (WD[i] ~ /^-[unsgkIPLdEa]$/) i++; i++ }
354
+ if (b == "timeout") i++
355
+ continue
356
+ }
357
+ break
358
+ }
359
+ if (i > NW) return
360
+ w = WD[i]; b = base(w)
361
+ if (b == "cd" || b == "pushd") {
362
+ if (XARGS) return
363
+ t = WD[i + 1]; if (t == "-P" || t == "-L" || t == "--") t = WD[i + 2]
364
+ if (t == "-") return
365
+ t = expand(t); if (index(t, "$") > 0) return
366
+ CUR = resolve(t); return
367
+ }
368
+ if (b == "git") {
369
+ j = i + 1; gd = CUR
370
+ while (j <= NW && WD[j] ~ /^-/) { if (WD[j] == "-C") { gd = resolve(WD[j + 1]); j++ } else if (WD[j] == "-c") j++; j++ }
371
+ if (WD[j] != "grep") return
372
+ t = CUR; CUR = gd; search("gitgrep", j + 1); CUR = t; return
373
+ }
374
+ if (b == "grep" || b == "egrep" || b == "fgrep") { search("grep", i + 1); return }
375
+ if (b == "rg" || b == "ag" || b == "ack") { search(b, i + 1); return }
376
+ if (b == "ack-grep") { search("ack", i + 1); return }
377
+ if (XARGS) return
378
+ if (b == "batcat") b = "bat"
379
+ if (b == "gawk" || b == "mawk" || b == "nawk") b = "awk"
380
+ if (b == "cat" || b == "bat" || b == "less" || b == "more" || b == "nl" || b == "head" || b == "tail" || b == "sed" || b == "awk") { readcmd(b, i + 1); return }
381
+ if (b == "find") { findcmd(i + 1); return }
382
+ }
383
+ function run( k, rd) {
384
+ NW = 0; NIN = 0; PIN = 0; rd = ""
385
+ for (k = 1; k <= NT + 1; k++) {
386
+ if (k > NT || TT[k] == "o") {
387
+ if (NW > 0) segment()
388
+ NW = 0; NIN = 0; rd = ""
389
+ PIN = (k <= NT && TV[k] == "|") ? 1 : 0
390
+ continue
391
+ }
392
+ if (TT[k] == "r") { rd = TV[k]; continue }
393
+ if (rd != "") { if (rd == "<") IN[++NIN] = TV[k]; rd = ""; continue }
394
+ WD[++NW] = TV[k]
395
+ }
396
+ }
397
+ { S = S $0 "\n" }
398
+ END {
399
+ setof("${SOURCE_EXTENSIONS.join(" ")}", SRC); setof("${SOURCE_TYPES.join(" ")}", STY); setof("${EXCLUDED_FOLDERS.join(" ")}", EXC); setof("${EXCLUDED_AT_TOP.join(" ")}", TOP)
400
+ HOME = ENVIRON["HOME"]
401
+ CMD = jstr(S, "command"); if (CMD == "") exit
402
+ CUR = jstr(S, "cwd"); if (CUR == "") CUR = C
403
+ gsub(/\001/, "\\", CUR)
404
+ lex(CMD); run()
405
+ }
406
+ `;
407
+
408
+ /** The reminder text. Built with JSON.stringify so it is valid JSON; no apostrophe, as it sits in single quotes. */
409
+ const NIRO_SHELL_SEARCH_JSON = JSON.stringify({
410
+ hookSpecificOutput: {
411
+ hookEventName: "PreToolUse",
412
+ additionalContext:
413
+ "NIRO ACTIVE. This shell command searches or reads source code on a niro-indexed repo. Prefer niro " +
414
+ "tools, every time and mid-task too: code_grep (text in code), get_source_code or resolve_fqn (a known " +
415
+ "symbol or a line range), get_call_chain, get_reverse_call_chain, get_blast_radius, get_impact_analysis " +
416
+ "(how code connects). Shell tools only for config, docs, logs, generated or non-code text, or a file " +
417
+ "you just edited.",
418
+ },
419
+ });
420
+
421
+ /** The hook's identity token, matched by isNiroShellSearchHook. */
422
+ const SHELL_SEARCH_TOKEN = "niro-shell-search-";
423
+
424
+ /**
425
+ * The hook command (matcher Bash). The awk program prints the paths; the loop takes the first that
426
+ * exists (and, for a file, was not changed in the last 10 minutes) and asks whether it lies under an
427
+ * indexed folder. Only that one path is walked, with the walk that starts no process per folder level
428
+ * (localMode FAST_CLOUD_WALK): walking all four paths with the explore hook's walk put the slowest 1 in
429
+ * 100 calls at 75 ms in the trial.
430
+ * `IFS` is set to a newline so a path with a space survives the loop; `set -f` stops the shell from
431
+ * expanding a `*` that reached it. Every way out is `exit 0`: exit 2 would block the command.
432
+ */
433
+ const NIRO_SHELL_SEARCH_COMMAND = [
434
+ 'IN="$(cat)"',
435
+ `T="$(printf '%s' "$IN" | awk -v C="\${CLAUDE_PROJECT_DIR:-$PWD}" '${AWK_PROGRAM}' 2>/dev/null)"`,
436
+ '[ -z "$T" ] && exit 0',
437
+ `set -f; IFS="$(printf '\\n.')"; IFS="\${IFS%.}"; ok=0`,
438
+ 'for t in $T; do k="${t%%/*}"; p="/${t#*/}"; ' +
439
+ 'if [ "$k" = D ]; then [ -d "$p" ] || continue; d0="$p"; ' +
440
+ 'else [ -f "$p" ] || continue; [ -n "$(find "$p" -mmin -10 2>/dev/null)" ] && continue; d0="$(dirname "$p")"; fi; ' +
441
+ `d0="$(cd -P "$d0" 2>/dev/null && pwd -P || printf '%s' "$d0")"; ` +
442
+ `${FAST_CLOUD_WALK}; break; done`,
443
+ '[ "$ok" = 1 ] || exit 0',
444
+ `NIRO_HOOK_ID=${SHELL_SEARCH_TOKEN}reminder`,
445
+ `printf '%s' '${NIRO_SHELL_SEARCH_JSON}'`,
446
+ 'exit 0',
447
+ ].join("; ");
448
+
449
+ for (const [what, text] of [["awk program", AWK_PROGRAM], ["reminder", NIRO_SHELL_SEARCH_JSON]]) {
450
+ if (text.includes("'")) throw new Error(`the shell search hook ${what} holds a single quote, which ends its shell quoting`);
451
+ }
452
+ for (const other of ["niro-explore-", "niro-impact-", "should_create_temp_project", "should_sandbox_repo"]) {
453
+ if (NIRO_SHELL_SEARCH_COMMAND.includes(other)) throw new Error(`the shell search hook holds ${other}, another hook's identity token`);
454
+ }
455
+
456
+ /** True if `h` is niro's Bash shell-search reminder entry. A foreign Bash hook is never matched. */
457
+ function isNiroShellSearchHook(h) {
458
+ const cmds = [];
459
+ if (h && Array.isArray(h.hooks)) h.hooks.forEach((i) => i && typeof i.command === "string" && cmds.push(i.command));
460
+ if (h && typeof h.command === "string") cmds.push(h.command);
461
+ return cmds.some((c) => c.includes(SHELL_SEARCH_TOKEN));
462
+ }
463
+
464
+ /**
465
+ * Adds the PreToolUse (Bash) shell-search reminder to ~/.claude/settings.json, the way the explore hook
466
+ * is added: the same command already there is left alone, an older niro entry is replaced in place, a
467
+ * foreign entry is never touched, and an unreadable file is started fresh.
468
+ */
469
+ function addClaudeCodeShellSearchHook(hookCommand = NIRO_SHELL_SEARCH_COMMAND) {
470
+ const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
471
+ let settings = {};
472
+ try {
473
+ if (fs.existsSync(settingsPath)) settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
474
+ } catch {
475
+ // Corrupt or missing: start fresh, as the other hook installers do.
476
+ }
477
+ if (!settings.hooks) settings.hooks = {};
478
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
479
+
480
+ const entry = { matcher: "Bash", hooks: [{ type: "command", command: hookCommand }] };
481
+ const alreadyCurrent = settings.hooks.PreToolUse.some(
482
+ (h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === hookCommand),
483
+ );
484
+ if (alreadyCurrent) {
485
+ console.log(" PreToolUse shell-search hook already up to date. Skipping.\n");
486
+ return;
487
+ }
488
+ const hadStale = settings.hooks.PreToolUse.some(isNiroShellSearchHook);
489
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !isNiroShellSearchHook(h));
490
+ settings.hooks.PreToolUse.push(entry);
491
+ try {
492
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
493
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
494
+ console.log(` PreToolUse shell-search hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
495
+ } catch (err) {
496
+ console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
497
+ console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
498
+ console.warn(JSON.stringify(entry, null, 2) + "\n");
499
+ }
500
+ }
501
+
502
+ module.exports = {
503
+ NIRO_SHELL_SEARCH_COMMAND,
504
+ NIRO_SHELL_SEARCH_JSON,
505
+ SHELL_SEARCH_TOKEN,
506
+ AWK_PROGRAM,
507
+ isNiroShellSearchHook,
508
+ addClaudeCodeShellSearchHook,
509
+ };