@niroai/niro 0.17.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.
- package/CHANGELOG.md +136 -0
- package/README.md +123 -302
- package/bin/niro.js +12 -1
- package/docs/agent-antigravity.md +37 -0
- package/docs/agent-claude-code.md +48 -0
- package/docs/agent-claude-desktop.md +47 -0
- package/docs/agent-cline.md +42 -0
- package/docs/agent-codex.md +45 -0
- package/docs/agent-copilot-cli.md +42 -0
- package/docs/agent-cursor.md +44 -0
- package/docs/agent-factory.md +42 -0
- package/docs/agent-gemini-cli.md +42 -0
- package/docs/agent-goose.md +42 -0
- package/docs/agent-kiro.md +37 -0
- package/docs/agent-opencode.md +37 -0
- package/docs/agent-vscode.md +45 -0
- package/docs/agent-windsurf.md +38 -0
- package/docs/answer-shapes.md +69 -0
- package/docs/builds-and-updates.md +68 -0
- package/docs/cloud-README.md +372 -0
- package/docs/configuring-a-folder.md +51 -0
- package/docs/connect-ci.md +24 -0
- package/docs/connect-overview.md +20 -0
- package/docs/connect-review-bots.md +17 -0
- package/docs/connect-team-server.md +12 -0
- package/docs/connect-terminal.md +49 -0
- package/docs/connect-web-agents.md +9 -0
- package/docs/connect-your-ai-assistant.md +59 -175
- package/docs/coverage-audit-and-consent.md +51 -0
- package/docs/evaluate-what-niro-did.md +271 -0
- package/docs/faq-assistants-and-mcp.md +122 -0
- package/docs/faq-feedback-and-receipts.md +33 -0
- package/docs/faq-indexing.md +127 -0
- package/docs/faq-install-and-setup.md +103 -0
- package/docs/faq-licence-and-plans.md +86 -0
- package/docs/faq-login-and-account.md +69 -0
- package/docs/faq-offline.md +34 -0
- package/docs/faq-performance.md +33 -0
- package/docs/faq-privacy-and-security.md +113 -0
- package/docs/faq-uninstall.md +47 -0
- package/docs/faq-updates.md +59 -0
- package/docs/faq-worktrees-and-branches.md +59 -0
- package/docs/faq-wrong-or-missing-answers.md +275 -0
- package/docs/hosted-only.md +33 -0
- package/docs/how-the-graph-works.md +47 -0
- package/docs/index.md +157 -0
- package/docs/licence-and-plans.md +72 -0
- package/docs/lifecycle-debug.md +48 -0
- package/docs/lifecycle-onboarding.md +32 -0
- package/docs/lifecycle-overview.md +77 -0
- package/docs/lifecycle-plan-and-design.md +44 -0
- package/docs/lifecycle-review.md +30 -0
- package/docs/lifecycle-security-and-privacy.md +35 -0
- package/docs/lifecycle-test.md +28 -0
- package/docs/lifecycle-write-code.md +35 -0
- package/docs/llms-full.txt +8015 -0
- package/docs/llms.txt +126 -0
- package/docs/playbook-add-a-feature.md +108 -0
- package/docs/playbook-change-safely.md +93 -0
- package/docs/playbook-data-and-events.md +107 -0
- package/docs/playbook-debug-after-a-change.md +96 -0
- package/docs/playbook-debug-config.md +95 -0
- package/docs/playbook-debug-exception.md +124 -0
- package/docs/playbook-debug-failing-service-call.md +119 -0
- package/docs/playbook-debug-slow-path.md +80 -0
- package/docs/playbook-debug-wrong-value.md +137 -0
- package/docs/playbook-onboard.md +95 -0
- package/docs/playbook-review-a-change.md +72 -0
- package/docs/playbook-test-a-change.md +73 -0
- package/docs/reference-cli.md +113 -0
- package/docs/reference-config-files.md +42 -0
- package/docs/reference-engine-commands.md +56 -0
- package/docs/reference-environment-variables.md +89 -0
- package/docs/reference-exit-codes.md +67 -0
- package/docs/reference-limits.md +75 -0
- package/docs/reference-mcp-tools.md +326 -0
- package/docs/reference-messages.md +180 -0
- package/docs/secrets-and-masking.md +48 -0
- package/docs/supported-c.md +130 -0
- package/docs/supported-cpp.md +157 -0
- package/docs/supported-csharp.md +211 -0
- package/docs/supported-go.md +258 -0
- package/docs/supported-java.md +226 -0
- package/docs/supported-javascript.md +223 -0
- package/docs/supported-kotlin.md +124 -0
- package/docs/supported-overview.md +89 -0
- package/docs/supported-php.md +157 -0
- package/docs/supported-python.md +224 -0
- package/docs/supported-ruby.md +162 -0
- package/docs/supported-rust.md +150 -0
- package/docs/supported-swift.md +133 -0
- package/docs/supported-typescript.md +220 -0
- package/docs/tokens-speed-and-accuracy.md +99 -0
- package/docs/troubleshooting.md +42 -0
- package/docs/uninstall.md +74 -0
- package/docs/what-leaves-your-machine.md +71 -0
- package/docs/where-things-are-stored.md +59 -0
- package/docs/worktrees-and-branches.md +55 -0
- package/package.json +5 -3
- package/src/commands/add-repo.js +5 -0
- package/src/commands/auditSend.js +67 -0
- package/src/commands/build.js +6 -0
- package/src/commands/docsSend.js +73 -0
- package/src/commands/engine.js +120 -0
- package/src/commands/engineRoutes.js +105 -0
- package/src/commands/guide.js +254 -0
- package/src/commands/init.js +6 -2
- package/src/commands/initLocal.js +96 -0
- package/src/commands/journalSend.js +47 -0
- package/src/commands/login.js +89 -12
- package/src/commands/mcp.js +22 -6
- package/src/commands/plan.js +99 -0
- package/src/commands/repoPush.js +33 -0
- package/src/commands/requestIntegration.js +38 -0
- package/src/commands/share.js +56 -0
- package/src/commands/start.js +451 -371
- package/src/commands/status.js +57 -0
- package/src/commands/takeover.js +6 -0
- package/src/commands/uninstall.js +180 -0
- package/src/commands/upgrade.js +38 -8
- package/src/commands/watch.js +420 -6
- package/src/index.js +174 -3
- package/src/lib/agentApi.js +32 -1
- package/src/lib/api.js +20 -6
- package/src/lib/auditUpload.js +293 -0
- package/src/lib/branchDrift.js +149 -0
- package/src/lib/choose.js +116 -0
- package/src/lib/cloudGuard.js +74 -0
- package/src/lib/credentials.js +2 -2
- package/src/lib/docsMarked.js +55 -0
- package/src/lib/docsState.js +213 -0
- package/src/lib/docsUpload.js +481 -0
- package/src/lib/engine.js +619 -0
- package/src/lib/folderSync.js +2 -1
- package/src/lib/graphCheck.js +40 -0
- package/src/lib/guide.js +293 -0
- package/src/lib/guideSearch.js +252 -0
- package/src/lib/integrationRequest.js +89 -0
- package/src/lib/journalUpload.js +147 -0
- package/src/lib/languages.js +129 -0
- package/src/lib/lease.js +375 -0
- package/src/lib/localBuild.js +285 -0
- package/src/lib/localEngine.js +210 -0
- package/src/lib/localOnboard.js +124 -0
- package/src/lib/localWorktrees.js +243 -0
- package/src/lib/machineCheck.js +58 -0
- package/src/lib/mcpClients.js +10 -1
- package/src/lib/migrationNotice.js +118 -0
- package/src/lib/modelFetch.js +153 -0
- package/src/lib/nestedOutput.js +23 -9
- package/src/lib/onboardingLog.js +34 -0
- package/src/lib/planUsage.js +165 -0
- package/src/lib/progressBar.js +46 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/redact.js +155 -0
- package/src/lib/release.js +33 -0
- package/src/lib/repoPush.js +495 -0
- package/src/lib/secret.js +1 -1
- package/src/lib/service.js +372 -63
- package/src/lib/shareCode.js +367 -0
- package/src/lib/shareExclusions.js +114 -0
- package/src/lib/shareFiles.js +129 -0
- package/src/lib/spinner.js +9 -5
- package/src/lib/watchDaemon.js +33 -10
- package/src/lib/watchState.js +12 -1
- package/src/lib/windowsPaths.js +67 -0
- package/src/mcp/lineCheck.js +382 -0
- package/src/mcp/lineName.js +92 -0
- package/src/mcp/localBackend.js +751 -0
- package/src/mcp/localMode.js +383 -0
- package/src/mcp/localSkill.js +189 -0
- package/src/mcp/server.js +610 -79
- package/src/mcp/setup.js +519 -131
- package/src/mcp/shellSearchHook.js +509 -0
- package/src/mcp/teardown.js +173 -91
- package/src/mcp/usingNiro.js +39 -0
- 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
|
+
};
|