@allansantos-dev/smart-tool 0.9.6 → 0.9.8
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 +38 -0
- package/README.md +7 -2
- package/block_patterns.py +266 -0
- package/config.py +13 -0
- package/hook_decision.py +12 -18
- package/index_scope.py +26 -0
- package/indexer.py +17 -1
- package/install.py +17 -0
- package/package.json +1 -1
- package/redirect_rule.py +232 -0
- package/router.py +29 -357
- package/setup_ui.py +84 -3
- package/smart_tool_daemon.py +15 -2
- package/version.py +1 -1
package/setup_ui.py
CHANGED
|
@@ -19,6 +19,7 @@ import re
|
|
|
19
19
|
import secrets
|
|
20
20
|
|
|
21
21
|
import autostart
|
|
22
|
+
import block_patterns
|
|
22
23
|
import client_hooks
|
|
23
24
|
import config
|
|
24
25
|
import daemon_launcher
|
|
@@ -185,19 +186,36 @@ def handle_integration_get():
|
|
|
185
186
|
duplicate_mode, duplicate_error = config.duplicate_mode(), ""
|
|
186
187
|
except ValueError as exc:
|
|
187
188
|
duplicate_mode, duplicate_error = "", str(exc)
|
|
189
|
+
try:
|
|
190
|
+
proposals_mode, proposals_error = config.pattern_proposals(), ""
|
|
191
|
+
except ValueError as exc:
|
|
192
|
+
proposals_mode, proposals_error = "", str(exc)
|
|
193
|
+
try:
|
|
194
|
+
stored, patterns_error = block_patterns.load(), ""
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
stored, patterns_error = block_patterns._empty(), str(exc)
|
|
188
197
|
return {"daemon_url": setup_origin(), "clients": clients, "hook_mode": mode, "hook_mode_error": mode_error,
|
|
189
198
|
"hook_modes": list(config.HOOK_MODES), "doc_mode": doc_mode, "doc_mode_error": doc_error,
|
|
190
199
|
"doc_modes": list(config.DOC_MODES), "duplicate_mode": duplicate_mode,
|
|
191
|
-
"duplicate_mode_error": duplicate_error, "duplicate_modes": list(config.DUPLICATE_MODES)
|
|
200
|
+
"duplicate_mode_error": duplicate_error, "duplicate_modes": list(config.DUPLICATE_MODES),
|
|
201
|
+
"pattern_proposals": proposals_mode, "pattern_proposals_error": proposals_error,
|
|
202
|
+
"block_patterns": {"proposals": stored["proposals"], "accepted": stored["accepted"],
|
|
203
|
+
"rejected": len(stored["rejected"]), "error": patterns_error}}
|
|
192
204
|
|
|
193
205
|
|
|
194
206
|
def handle_integration_save(body):
|
|
195
207
|
"""`hook_mode` sets redirect/advise/off for every client; `register_mcp` runs the client's CLI; hook in two steps:
|
|
196
208
|
`preview` shows the exact change, `install` writes it (with backup)."""
|
|
197
|
-
if isinstance(body, dict) and body.get("action")
|
|
209
|
+
if isinstance(body, dict) and body.get("action") == "block_pattern":
|
|
210
|
+
try:
|
|
211
|
+
block_patterns.decide_proposal(str(body.get("id") or ""), body.get("decision"))
|
|
212
|
+
except (OSError, ValueError) as exc:
|
|
213
|
+
raise SetupError(str(exc))
|
|
214
|
+
return handle_integration_get()
|
|
215
|
+
if isinstance(body, dict) and body.get("action") in ("hook_mode", "doc_mode", "duplicate_mode", "pattern_proposals"):
|
|
198
216
|
action = body["action"]
|
|
199
217
|
allowed = {"hook_mode": config.HOOK_MODES, "doc_mode": config.DOC_MODES,
|
|
200
|
-
"duplicate_mode": config.DUPLICATE_MODES}[action]
|
|
218
|
+
"duplicate_mode": config.DUPLICATE_MODES, "pattern_proposals": config.PATTERN_PROPOSAL_MODES}[action]
|
|
201
219
|
if body.get("mode") not in allowed:
|
|
202
220
|
raise SetupError(f"Invalid mode: use {', '.join(allowed)}.")
|
|
203
221
|
try:
|
|
@@ -593,6 +611,14 @@ _PAGE_TEMPLATE = """<!doctype html>
|
|
|
593
611
|
</select>
|
|
594
612
|
<span class="hint">Compares functions the edit writes with the indexed code: identical bodies, and near-identical ones with only local names changed. Never blocks.</span></div>
|
|
595
613
|
<div class="actions"><button type="button" id="btn-save-duplicate-mode">Save duplicates</button></div>
|
|
614
|
+
<div class="field" style="margin-top:16px"><label for="sel-pattern-proposals">Block patterns proposed by a model</label>
|
|
615
|
+
<select id="sel-pattern-proposals">
|
|
616
|
+
<option value="on">On: searches the rule lets run are reviewed in the background and may become proposals (default)</option>
|
|
617
|
+
<option value="off">Off: no review</option>
|
|
618
|
+
</select>
|
|
619
|
+
<span class="hint">The hook decides with a fixed rule: content searches over a project folder go to smart_search. The scope model (else the router model) never delays a call: it reviews, afterwards, bulk reads the rule could not measure and proposes patterns, each checked against the logged calls. Nothing blocks until you accept it here.</span></div>
|
|
620
|
+
<div class="actions"><button type="button" id="btn-save-pattern-proposals">Save proposals setting</button></div>
|
|
621
|
+
<div id="patterns-list"></div>
|
|
596
622
|
<div class="msg" id="agents-msg" role="status"></div>
|
|
597
623
|
<div id="agents-list"></div>
|
|
598
624
|
</section>
|
|
@@ -882,6 +908,9 @@ async function loadAgents() {
|
|
|
882
908
|
if (data.hook_mode_error) setMsg("agents-msg", "error", data.hook_mode_error);
|
|
883
909
|
if (data.doc_mode_error) setMsg("agents-msg", "error", data.doc_mode_error);
|
|
884
910
|
if (data.duplicate_mode_error) setMsg("agents-msg", "error", data.duplicate_mode_error);
|
|
911
|
+
if (document.activeElement !== $("sel-pattern-proposals")) $("sel-pattern-proposals").value = data.pattern_proposals;
|
|
912
|
+
if (data.pattern_proposals_error) setMsg("agents-msg", "error", data.pattern_proposals_error);
|
|
913
|
+
renderPatterns(data.block_patterns);
|
|
885
914
|
const list = $("agents-list");
|
|
886
915
|
list.textContent = "";
|
|
887
916
|
for (const item of data.clients) {
|
|
@@ -944,7 +973,59 @@ async function loadAgents() {
|
|
|
944
973
|
setMsg("agents-msg", "error", "Agent status unavailable: " + error.message);
|
|
945
974
|
}
|
|
946
975
|
}
|
|
976
|
+
function renderPatterns(patterns) {
|
|
977
|
+
const box = $("patterns-list");
|
|
978
|
+
box.textContent = "";
|
|
979
|
+
if (patterns.error) { const p = document.createElement("p"); p.className = "msg error"; p.textContent = patterns.error; box.appendChild(p); return; }
|
|
980
|
+
const groups = [["proposals", "Proposed", [["accept", "Accept"], ["reject", "Reject"]]], ["accepted", "Blocking", [["remove", "Remove"]]]];
|
|
981
|
+
for (const [key, title, actions] of groups) {
|
|
982
|
+
if (!patterns[key].length) continue;
|
|
983
|
+
const head = document.createElement("h3");
|
|
984
|
+
head.textContent = title + " (" + patterns[key].length + ")";
|
|
985
|
+
box.appendChild(head);
|
|
986
|
+
for (const item of patterns[key]) {
|
|
987
|
+
const row = document.createElement("div");
|
|
988
|
+
row.className = "provider";
|
|
989
|
+
const code = document.createElement("code");
|
|
990
|
+
code.textContent = item.tool + ": " + item.pattern;
|
|
991
|
+
const why = document.createElement("p");
|
|
992
|
+
const seen = item.history_total ? " Would have blocked " + item.history_matches + " of " + item.history_total + " logged " + item.tool + " calls." : "";
|
|
993
|
+
why.textContent = item.reason + seen;
|
|
994
|
+
const example = document.createElement("p");
|
|
995
|
+
example.className = "hint";
|
|
996
|
+
example.textContent = "Example: " + item.example;
|
|
997
|
+
row.append(code, why, example);
|
|
998
|
+
const buttons = document.createElement("div");
|
|
999
|
+
buttons.className = "actions";
|
|
1000
|
+
for (const [decision, label] of actions) {
|
|
1001
|
+
const button = document.createElement("button");
|
|
1002
|
+
button.type = "button";
|
|
1003
|
+
button.textContent = label;
|
|
1004
|
+
button.addEventListener("click", async () => {
|
|
1005
|
+
try {
|
|
1006
|
+
renderPatterns((await post("/setup/integration", { action: "block_pattern", id: item.id, decision })).block_patterns);
|
|
1007
|
+
setMsg("agents-msg", "ok", label + ": " + item.pattern);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
setMsg("agents-msg", "error", error.message);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
buttons.appendChild(button);
|
|
1013
|
+
}
|
|
1014
|
+
row.appendChild(buttons);
|
|
1015
|
+
box.appendChild(row);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (patterns.rejected) { const p = document.createElement("p"); p.className = "hint"; p.textContent = patterns.rejected + " rejected proposal(s) are not offered again."; box.appendChild(p); }
|
|
1019
|
+
}
|
|
947
1020
|
loadAgents();
|
|
1021
|
+
$("btn-save-pattern-proposals").addEventListener("click", async () => {
|
|
1022
|
+
try {
|
|
1023
|
+
await post("/setup/integration", { action: "pattern_proposals", mode: $("sel-pattern-proposals").value });
|
|
1024
|
+
setMsg("agents-msg", "ok", "Proposals setting saved.");
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
setMsg("agents-msg", "error", error.message);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
948
1029
|
$("btn-save-doc-mode").addEventListener("click", async () => {
|
|
949
1030
|
try {
|
|
950
1031
|
await post("/setup/integration", { action: "doc_mode", mode: $("sel-doc-mode").value });
|
package/smart_tool_daemon.py
CHANGED
|
@@ -169,6 +169,10 @@ _jobs = {}
|
|
|
169
169
|
_JOBS_LOCK = threading.Lock()
|
|
170
170
|
_MAX_JOBS_RETAINED = 200
|
|
171
171
|
SMART_SEARCH_SYNC_WAIT_S = 3
|
|
172
|
+
# smart_search_result waits for the job this long before answering pending: answering at once had an agent poll the
|
|
173
|
+
# same job 7 times in 16 s and then give up on Smart Tool (claude -p session, 2026-10-09; job durations median 6 s,
|
|
174
|
+
# p75 10.7 s).
|
|
175
|
+
SMART_SEARCH_RESULT_WAIT_S = 15
|
|
172
176
|
SMART_SEARCH_MAX_PENDING = 8
|
|
173
177
|
SMART_SEARCH_EXECUTOR = project_scheduler.ProjectScheduler(workers=3)
|
|
174
178
|
_PROJECT_LOCKS = {}
|
|
@@ -305,8 +309,8 @@ def _smart_search_job_payload(job_id):
|
|
|
305
309
|
ALWAYS_LOAD = {"anthropic/alwaysLoad": True}
|
|
306
310
|
SERVER_INSTRUCTIONS = (
|
|
307
311
|
"Smart Tool serves an indexed semantic search over the user's projects and a cheaper web reader. The user installed "
|
|
308
|
-
"a PreToolUse hook that blocks
|
|
309
|
-
"WebFetch/WebSearch. A call blocked by Smart Tool is a routing rule, not a failure: make the call the block "
|
|
312
|
+
"a PreToolUse hook that blocks content searches over a project folder (grep -r, rg, git grep, Grep on a folder) "
|
|
313
|
+
"and native WebFetch/WebSearch. A call blocked by Smart Tool is a routing rule, not a failure: make the call the block "
|
|
310
314
|
"message names (smart_search with project_root and query_identifiers, web_fetch with url and prompt, web_search "
|
|
311
315
|
"with query). Never retry the same search or download through Bash, python, node, PowerShell, curl or wget: that "
|
|
312
316
|
"bypasses the rule the user set. Reading one known file, or searching inside one known file, is never blocked. "
|
|
@@ -807,6 +811,11 @@ def _prepare_project_index(arguments, job_id, cfg, token, query_vector=None, pre
|
|
|
807
811
|
checkpoint()
|
|
808
812
|
_update_job(job_id, phase="scope")
|
|
809
813
|
scope = index_scope.load_scope(root)
|
|
814
|
+
if scope is None and not arguments.get("force_scope"):
|
|
815
|
+
inherited = index_scope.sibling_scope(root)
|
|
816
|
+
if inherited is not None and not index_scope.needs_rescan(root, inherited):
|
|
817
|
+
index_scope.save_scope(root, inherited)
|
|
818
|
+
scope = inherited
|
|
810
819
|
scope_model = cfg.get("scope_model") or cfg.get("router_model")
|
|
811
820
|
if arguments.get("force_scope") or (project.get('scope_dirty') and not (scope or {}).get('manual')) or scope is None or index_scope.needs_rescan(root, scope):
|
|
812
821
|
problem = _gateway_problem() if scope_model else None
|
|
@@ -1342,6 +1351,10 @@ def _handle_smart_search_result(arguments):
|
|
|
1342
1351
|
if not job_id:
|
|
1343
1352
|
raise ValueError("smart_search_result requires 'job_id'")
|
|
1344
1353
|
payload = _smart_search_job_payload(job_id)
|
|
1354
|
+
deadline = time.monotonic() + SMART_SEARCH_RESULT_WAIT_S
|
|
1355
|
+
while payload["status"] == "pending" and time.monotonic() < deadline:
|
|
1356
|
+
time.sleep(0.25)
|
|
1357
|
+
payload = _smart_search_job_payload(job_id)
|
|
1345
1358
|
_mark_notice_delivered(job_id)
|
|
1346
1359
|
return _to_json_sanitized(payload)
|
|
1347
1360
|
|
package/version.py
CHANGED