@allansantos-dev/smart-tool 0.9.0 → 0.9.2

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 (41) hide show
  1. package/CHANGELOG.md +50 -23
  2. package/NOTICE +32 -32
  3. package/README.md +117 -116
  4. package/atomic_io.py +94 -94
  5. package/bin/smart-tool.mjs +5 -1
  6. package/client_hooks.py +192 -192
  7. package/code_graph_java.mjs +49 -49
  8. package/code_graph_js.cjs +135 -135
  9. package/duplicates.py +313 -313
  10. package/embedding_cache.py +99 -99
  11. package/endpoint_sync.py +179 -179
  12. package/gateway.py +220 -220
  13. package/hook_decision.py +96 -96
  14. package/index_inventory.py +75 -75
  15. package/indexer.py +878 -878
  16. package/install.py +113 -11
  17. package/install_runtime.py +101 -35
  18. package/integration_metrics.py +82 -82
  19. package/local_embedder.py +172 -172
  20. package/model_defaults.py +70 -70
  21. package/package.json +35 -35
  22. package/project_insights_ui.py +38 -38
  23. package/project_monitor.py +204 -204
  24. package/project_store.py +220 -220
  25. package/projects_ui.py +146 -146
  26. package/requirements.txt +8 -8
  27. package/research_cache.py +171 -171
  28. package/research_metrics.py +110 -110
  29. package/router.py +582 -582
  30. package/secret_store.py +121 -121
  31. package/setup_ui.py +1011 -986
  32. package/smart_tool_daemon.py +3604 -3604
  33. package/version.py +1 -1
  34. package/web_adapters/browser/fetch_camoufox.py +69 -0
  35. package/web_adapters/node/ddg-search.mjs +89 -89
  36. package/web_adapters/node/open-websearch-adapter.mjs +77 -77
  37. package/web_adapters/node/package.json +12 -12
  38. package/web_adapters/node/wikipedia-search.mjs +44 -44
  39. package/web_search_adapters.py +193 -193
  40. package/web_search_external.py +288 -288
  41. package/web_search_health.py +88 -88
package/local_embedder.py CHANGED
@@ -1,172 +1,172 @@
1
- """Embedder local do mcp-memory (sidecar granite em ~/.mcp-memory/embedding.properties) como reserva sem modelo no gateway.
2
-
3
- Medido em 2026-10-05 no gabarito do rerank (42 alvos de código, sem rerank, docs/spikes/local_embedder_spike.py):
4
- granite-embedding-107m-multilingual local 42/35/41 (no pool / 1º / top 5), MRR@5 0,892; gateway
5
- text-embedding-3-small do gateway 42/35/40, 0,885; só lexical 38/29/36, 0,767.
6
- """
7
- import http.client
8
- import json
9
- import os
10
- import threading
11
- import time
12
- import urllib.parse
13
-
14
- import config
15
- import indexer
16
-
17
- PREFIX = "local:"
18
- PROPERTIES = os.path.join(os.path.expanduser("~"), ".mcp-memory", "embedding.properties")
19
- BATCH = 16
20
- INFO_TTL_S = 60
21
- MAX_ATTEMPTS = 3
22
- REQUEST_TIMEOUT_S = 60
23
- _lock = threading.Lock()
24
- _info = {"at": 0.0, "value": None}
25
-
26
-
27
- def url():
28
- """Sidecar base URL; None when absent or `tei.url=off`. localhost becomes 127.0.0.1: on Windows ::1 is tried
29
- first and a sidecar bound to IPv4 only costs ~2 s per connection (measured)."""
30
- try:
31
- with open(PROPERTIES, encoding="utf-8") as stream:
32
- for line in stream:
33
- key, _, value = line.strip().partition("=")
34
- value = value.strip()
35
- if key.strip() == "tei.url":
36
- if not value or value.lower() == "off":
37
- return None
38
- parsed = urllib.parse.urlsplit(value.rstrip("/"))
39
- if parsed.hostname == "localhost":
40
- parsed = parsed._replace(netloc=parsed.netloc.replace("localhost", "127.0.0.1", 1))
41
- return urllib.parse.urlunsplit(parsed)
42
- except OSError:
43
- return None
44
- return None
45
-
46
-
47
- def configured():
48
- return url() is not None
49
-
50
-
51
- def _connection(base, timeout):
52
- parsed = urllib.parse.urlsplit(base)
53
- cls = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
54
- return cls(parsed.hostname, parsed.port, timeout=timeout)
55
-
56
-
57
- def _request(conn, method, path, body=None):
58
- headers = {"Content-Type": "application/json"} if body is not None else {}
59
- conn.request(method, path, body=json.dumps(body).encode("utf-8") if body is not None else None, headers=headers)
60
- response = conn.getresponse()
61
- data = response.read()
62
- if response.status >= 400:
63
- raise ConnectionError(f"local embedder answered HTTP {response.status}")
64
- return json.loads(data)
65
-
66
-
67
- def _probe(base, timeout):
68
- conn = _connection(base, timeout)
69
- try:
70
- data = _request(conn, "GET", "/info")
71
- finally:
72
- conn.close()
73
- if not data.get("model_id") or int(data.get("dimension") or 0) <= 0:
74
- raise ValueError("local embedder has no model_id/dimension in /info")
75
- return {"model": PREFIX + data["model_id"], "dimensions": int(data["dimension"]), "url": base,
76
- "sha256": data.get("model_sha256")}
77
-
78
-
79
- def info(timeout=1.5):
80
- """Sidecar /info cached for INFO_TTL_S; None when not configured or not answering. The probe runs under the lock
81
- so a slow failing probe never overwrites a newer good answer."""
82
- with _lock:
83
- if time.monotonic() - _info["at"] < INFO_TTL_S:
84
- return _info["value"]
85
- base, value = url(), None
86
- if base:
87
- try:
88
- value = _probe(base, timeout)
89
- except (OSError, ValueError, http.client.HTTPException):
90
- value = None
91
- _info.update(at=time.monotonic(), value=value)
92
- return value
93
-
94
-
95
- def is_local(model):
96
- return bool(model) and model.startswith(PREFIX)
97
-
98
-
99
- def resolve(configured_model, current=None, mode=None):
100
- """Embedding model for the index, following config `local_models` (fallback | off | prefer).
101
-
102
- An index already built with the local model is kept while the sidecar is briefly down, so a restart of the
103
- sidecar never triggers a rebuild to lexical and back."""
104
- mode = mode or config.local_models_mode()
105
- if mode == "off":
106
- return configured_model or indexer.LEXICAL_MODEL
107
- if configured_model and mode == "fallback":
108
- return configured_model
109
- available = info()
110
- if available:
111
- return available["model"]
112
- if is_local(current):
113
- return current
114
- return configured_model or indexer.LEXICAL_MODEL
115
-
116
-
117
- def _left(deadline):
118
- if deadline is None:
119
- return REQUEST_TIMEOUT_S
120
- left = deadline - time.monotonic()
121
- if left <= 0:
122
- raise TimeoutError("Deadline exceeded before local embeddings finished.")
123
- return min(REQUEST_TIMEOUT_S, left)
124
-
125
-
126
- def embed(model, texts, deadline=None, cancel_check=None, on_retry=None):
127
- base = url()
128
- if not base:
129
- raise RuntimeError(f"mcp-memory local embedder is not configured ({PROPERTIES}).")
130
- # Fresh /info before and after: /embed does not echo the model, and a cached answer could let vectors of a
131
- # different model reach the persistent vector cache under this model's contract.
132
- try:
133
- before = _probe(base, _left(deadline))
134
- except (OSError, ValueError, http.client.HTTPException) as exc:
135
- raise RuntimeError(f"mcp-memory local embedder is not responding ({base}): {exc}") from None
136
- if before["model"] != model:
137
- raise indexer.IndexCompatibilityError(
138
- f"The local embedder is now {before['model']}; the index was built with {model} and must be rebuilt.")
139
- vectors = []
140
- conn = _connection(base, _left(deadline))
141
- try:
142
- for start in range(0, len(texts), BATCH):
143
- part = texts[start:start + BATCH]
144
- for attempt in range(1, MAX_ATTEMPTS + 1):
145
- if cancel_check:
146
- cancel_check()
147
- conn.timeout = _left(deadline)
148
- try:
149
- got = _request(conn, "POST", "/embed", {"inputs": part})
150
- break
151
- except (OSError, http.client.HTTPException) as exc:
152
- conn.close()
153
- conn = _connection(base, _left(deadline))
154
- if attempt == MAX_ATTEMPTS:
155
- raise RuntimeError(f"Local embedder failed after {MAX_ATTEMPTS} attempts: {exc}") from None
156
- if on_retry:
157
- on_retry(attempt + 1, len(part))
158
- delay = attempt
159
- if deadline is not None and time.monotonic() + delay >= deadline:
160
- raise TimeoutError("Not enough time left to retry the local embedder.") from None
161
- time.sleep(delay)
162
- if not isinstance(got, list) or len(got) != len(part):
163
- raise indexer.IndexVectorError("Local embedder returned a vector count different from the input.")
164
- for vector in got:
165
- indexer.validate_vector(vector, before["dimensions"])
166
- vectors += got
167
- finally:
168
- conn.close()
169
- after = _probe(base, _left(deadline))
170
- if (after["model"], after.get("sha256")) != (before["model"], before.get("sha256")):
171
- raise indexer.IndexCompatibilityError("The local embedder changed models during the call; try again.")
172
- return vectors
1
+ """Embedder local do mcp-memory (sidecar granite em ~/.mcp-memory/embedding.properties) como reserva sem modelo no gateway.
2
+
3
+ Medido em 2026-10-05 no gabarito do rerank (42 alvos de código, sem rerank, docs/spikes/local_embedder_spike.py):
4
+ granite-embedding-107m-multilingual local 42/35/41 (no pool / 1º / top 5), MRR@5 0,892; gateway
5
+ text-embedding-3-small do gateway 42/35/40, 0,885; só lexical 38/29/36, 0,767.
6
+ """
7
+ import http.client
8
+ import json
9
+ import os
10
+ import threading
11
+ import time
12
+ import urllib.parse
13
+
14
+ import config
15
+ import indexer
16
+
17
+ PREFIX = "local:"
18
+ PROPERTIES = os.path.join(os.path.expanduser("~"), ".mcp-memory", "embedding.properties")
19
+ BATCH = 16
20
+ INFO_TTL_S = 60
21
+ MAX_ATTEMPTS = 3
22
+ REQUEST_TIMEOUT_S = 60
23
+ _lock = threading.Lock()
24
+ _info = {"at": 0.0, "value": None}
25
+
26
+
27
+ def url():
28
+ """Sidecar base URL; None when absent or `tei.url=off`. localhost becomes 127.0.0.1: on Windows ::1 is tried
29
+ first and a sidecar bound to IPv4 only costs ~2 s per connection (measured)."""
30
+ try:
31
+ with open(PROPERTIES, encoding="utf-8") as stream:
32
+ for line in stream:
33
+ key, _, value = line.strip().partition("=")
34
+ value = value.strip()
35
+ if key.strip() == "tei.url":
36
+ if not value or value.lower() == "off":
37
+ return None
38
+ parsed = urllib.parse.urlsplit(value.rstrip("/"))
39
+ if parsed.hostname == "localhost":
40
+ parsed = parsed._replace(netloc=parsed.netloc.replace("localhost", "127.0.0.1", 1))
41
+ return urllib.parse.urlunsplit(parsed)
42
+ except OSError:
43
+ return None
44
+ return None
45
+
46
+
47
+ def configured():
48
+ return url() is not None
49
+
50
+
51
+ def _connection(base, timeout):
52
+ parsed = urllib.parse.urlsplit(base)
53
+ cls = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
54
+ return cls(parsed.hostname, parsed.port, timeout=timeout)
55
+
56
+
57
+ def _request(conn, method, path, body=None):
58
+ headers = {"Content-Type": "application/json"} if body is not None else {}
59
+ conn.request(method, path, body=json.dumps(body).encode("utf-8") if body is not None else None, headers=headers)
60
+ response = conn.getresponse()
61
+ data = response.read()
62
+ if response.status >= 400:
63
+ raise ConnectionError(f"local embedder answered HTTP {response.status}")
64
+ return json.loads(data)
65
+
66
+
67
+ def _probe(base, timeout):
68
+ conn = _connection(base, timeout)
69
+ try:
70
+ data = _request(conn, "GET", "/info")
71
+ finally:
72
+ conn.close()
73
+ if not data.get("model_id") or int(data.get("dimension") or 0) <= 0:
74
+ raise ValueError("local embedder has no model_id/dimension in /info")
75
+ return {"model": PREFIX + data["model_id"], "dimensions": int(data["dimension"]), "url": base,
76
+ "sha256": data.get("model_sha256")}
77
+
78
+
79
+ def info(timeout=1.5):
80
+ """Sidecar /info cached for INFO_TTL_S; None when not configured or not answering. The probe runs under the lock
81
+ so a slow failing probe never overwrites a newer good answer."""
82
+ with _lock:
83
+ if time.monotonic() - _info["at"] < INFO_TTL_S:
84
+ return _info["value"]
85
+ base, value = url(), None
86
+ if base:
87
+ try:
88
+ value = _probe(base, timeout)
89
+ except (OSError, ValueError, http.client.HTTPException):
90
+ value = None
91
+ _info.update(at=time.monotonic(), value=value)
92
+ return value
93
+
94
+
95
+ def is_local(model):
96
+ return bool(model) and model.startswith(PREFIX)
97
+
98
+
99
+ def resolve(configured_model, current=None, mode=None):
100
+ """Embedding model for the index, following config `local_models` (fallback | off | prefer).
101
+
102
+ An index already built with the local model is kept while the sidecar is briefly down, so a restart of the
103
+ sidecar never triggers a rebuild to lexical and back."""
104
+ mode = mode or config.local_models_mode()
105
+ if mode == "off":
106
+ return configured_model or indexer.LEXICAL_MODEL
107
+ if configured_model and mode == "fallback":
108
+ return configured_model
109
+ available = info()
110
+ if available:
111
+ return available["model"]
112
+ if is_local(current):
113
+ return current
114
+ return configured_model or indexer.LEXICAL_MODEL
115
+
116
+
117
+ def _left(deadline):
118
+ if deadline is None:
119
+ return REQUEST_TIMEOUT_S
120
+ left = deadline - time.monotonic()
121
+ if left <= 0:
122
+ raise TimeoutError("Deadline exceeded before local embeddings finished.")
123
+ return min(REQUEST_TIMEOUT_S, left)
124
+
125
+
126
+ def embed(model, texts, deadline=None, cancel_check=None, on_retry=None):
127
+ base = url()
128
+ if not base:
129
+ raise RuntimeError(f"mcp-memory local embedder is not configured ({PROPERTIES}).")
130
+ # Fresh /info before and after: /embed does not echo the model, and a cached answer could let vectors of a
131
+ # different model reach the persistent vector cache under this model's contract.
132
+ try:
133
+ before = _probe(base, _left(deadline))
134
+ except (OSError, ValueError, http.client.HTTPException) as exc:
135
+ raise RuntimeError(f"mcp-memory local embedder is not responding ({base}): {exc}") from None
136
+ if before["model"] != model:
137
+ raise indexer.IndexCompatibilityError(
138
+ f"The local embedder is now {before['model']}; the index was built with {model} and must be rebuilt.")
139
+ vectors = []
140
+ conn = _connection(base, _left(deadline))
141
+ try:
142
+ for start in range(0, len(texts), BATCH):
143
+ part = texts[start:start + BATCH]
144
+ for attempt in range(1, MAX_ATTEMPTS + 1):
145
+ if cancel_check:
146
+ cancel_check()
147
+ conn.timeout = _left(deadline)
148
+ try:
149
+ got = _request(conn, "POST", "/embed", {"inputs": part})
150
+ break
151
+ except (OSError, http.client.HTTPException) as exc:
152
+ conn.close()
153
+ conn = _connection(base, _left(deadline))
154
+ if attempt == MAX_ATTEMPTS:
155
+ raise RuntimeError(f"Local embedder failed after {MAX_ATTEMPTS} attempts: {exc}") from None
156
+ if on_retry:
157
+ on_retry(attempt + 1, len(part))
158
+ delay = attempt
159
+ if deadline is not None and time.monotonic() + delay >= deadline:
160
+ raise TimeoutError("Not enough time left to retry the local embedder.") from None
161
+ time.sleep(delay)
162
+ if not isinstance(got, list) or len(got) != len(part):
163
+ raise indexer.IndexVectorError("Local embedder returned a vector count different from the input.")
164
+ for vector in got:
165
+ indexer.validate_vector(vector, before["dimensions"])
166
+ vectors += got
167
+ finally:
168
+ conn.close()
169
+ after = _probe(base, _left(deadline))
170
+ if (after["model"], after.get("sha256")) != (before["model"], before.get("sha256")):
171
+ raise indexer.IndexCompatibilityError("The local embedder changed models during the call; try again.")
172
+ return vectors
package/model_defaults.py CHANGED
@@ -1,70 +1,70 @@
1
- """Preenche modelos recomendados somente após consultar o catálogo do gateway configurado."""
2
-
3
- import re
4
-
5
- import config
6
- import model_catalog
7
-
8
-
9
- # Matched as the last path/dot segment of the catalog id, so "gpt-4o-mini" also finds "openai/gpt-4o-mini" (OpenRouter)
10
- # and "openai.gpt-4o-mini" (LiteLLM aliases). Router: small, no reasoning (it runs before tool calls).
11
- RECOMMENDED_MODELS = {
12
- "router_model": ("chat", "gpt-4o-mini"),
13
- "scope_model": ("chat", "gpt-5-mini"),
14
- "research_model": ("chat", "gpt-oss-20b"),
15
- "embedding_model": ("embedding", "text-embedding-3-small"),
16
- "rerank_model": ("rerank", "rerank-3-5"),
17
- }
18
-
19
-
20
- def _matching(catalog, name):
21
- return next((model_id for model_id in sorted(catalog) if re.split(r"[/.:]", model_id)[-1] == name
22
- or model_id.endswith(("/" + name, "." + name)) or model_id == name), None)
23
-
24
-
25
- def ensure_recommended_models(key_ready):
26
- """Preenche só campos vazios e só com IDs presentes no catálogo permitido.
27
-
28
- Retorna os campos ainda vazios e uma explicação curta para a tela de setup/tray.
29
- Um arquivo corrompido ou uma seleção manual nunca é substituído.
30
- """
31
- corrupted = config.corrupted_reason()
32
- if corrupted:
33
- return {"status": "config_corrupt", "missing": list(RECOMMENDED_MODELS),
34
- "message": corrupted}
35
-
36
- cfg = config.load_config()
37
- missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
38
- if not key_ready:
39
- return {"status": "needs_key", "missing": missing,
40
- "message": "Model gateway unavailable; check the base URL and the API key."}
41
- if not missing:
42
- return {"status": "ready", "missing": [], "message": "Models configured."}
43
-
44
- try:
45
- available = {mode: set(model_catalog.list_models(mode)) for mode in
46
- {RECOMMENDED_MODELS[field][0] for field in missing}}
47
- except Exception:
48
- return {"status": "catalog_unavailable", "missing": missing,
49
- "message": "Catalog unavailable. Check the base URL, the API key and the connection to the gateway."}
50
-
51
- choices = {field: found for field in missing
52
- for mode, name in [RECOMMENDED_MODELS[field]]
53
- for found in [_matching(available[mode], name)] if found}
54
- if choices:
55
- def fill_empty(current):
56
- for field, model_id in choices.items():
57
- if not current.get(field):
58
- current[field] = model_id
59
-
60
- try:
61
- cfg = config.update_config(fill_empty)
62
- except (config.ConfigCorruptedError, OSError):
63
- return {"status": "config_corrupt", "missing": missing,
64
- "message": "Could not save the configuration. Open Smart Tool to check the file."}
65
-
66
- missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
67
- if missing:
68
- return {"status": "needs_models", "missing": missing,
69
- "message": "Some recommended models are not in this gateway's catalog. Choose them in the settings."}
70
- return {"status": "ready", "missing": [], "message": "Recommended models configured."}
1
+ """Preenche modelos recomendados somente após consultar o catálogo do gateway configurado."""
2
+
3
+ import re
4
+
5
+ import config
6
+ import model_catalog
7
+
8
+
9
+ # Matched as the last path/dot segment of the catalog id, so "gpt-4o-mini" also finds "openai/gpt-4o-mini" (OpenRouter)
10
+ # and "openai.gpt-4o-mini" (LiteLLM aliases). Router: small, no reasoning (it runs before tool calls).
11
+ RECOMMENDED_MODELS = {
12
+ "router_model": ("chat", "gpt-4o-mini"),
13
+ "scope_model": ("chat", "gpt-5-mini"),
14
+ "research_model": ("chat", "gpt-oss-20b"),
15
+ "embedding_model": ("embedding", "text-embedding-3-small"),
16
+ "rerank_model": ("rerank", "rerank-3-5"),
17
+ }
18
+
19
+
20
+ def _matching(catalog, name):
21
+ return next((model_id for model_id in sorted(catalog) if re.split(r"[/.:]", model_id)[-1] == name
22
+ or model_id.endswith(("/" + name, "." + name)) or model_id == name), None)
23
+
24
+
25
+ def ensure_recommended_models(key_ready):
26
+ """Preenche só campos vazios e só com IDs presentes no catálogo permitido.
27
+
28
+ Retorna os campos ainda vazios e uma explicação curta para a tela de setup/tray.
29
+ Um arquivo corrompido ou uma seleção manual nunca é substituído.
30
+ """
31
+ corrupted = config.corrupted_reason()
32
+ if corrupted:
33
+ return {"status": "config_corrupt", "missing": list(RECOMMENDED_MODELS),
34
+ "message": corrupted}
35
+
36
+ cfg = config.load_config()
37
+ missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
38
+ if not key_ready:
39
+ return {"status": "needs_key", "missing": missing,
40
+ "message": "Model gateway unavailable; check the base URL and the API key."}
41
+ if not missing:
42
+ return {"status": "ready", "missing": [], "message": "Models configured."}
43
+
44
+ try:
45
+ available = {mode: set(model_catalog.list_models(mode)) for mode in
46
+ {RECOMMENDED_MODELS[field][0] for field in missing}}
47
+ except Exception:
48
+ return {"status": "catalog_unavailable", "missing": missing,
49
+ "message": "Catalog unavailable. Check the base URL, the API key and the connection to the gateway."}
50
+
51
+ choices = {field: found for field in missing
52
+ for mode, name in [RECOMMENDED_MODELS[field]]
53
+ for found in [_matching(available[mode], name)] if found}
54
+ if choices:
55
+ def fill_empty(current):
56
+ for field, model_id in choices.items():
57
+ if not current.get(field):
58
+ current[field] = model_id
59
+
60
+ try:
61
+ cfg = config.update_config(fill_empty)
62
+ except (config.ConfigCorruptedError, OSError):
63
+ return {"status": "config_corrupt", "missing": missing,
64
+ "message": "Could not save the configuration. Open Smart Tool to check the file."}
65
+
66
+ missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
67
+ if missing:
68
+ return {"status": "needs_models", "missing": missing,
69
+ "message": "Some recommended models are not in this gateway's catalog. Choose them in the settings."}
70
+ return {"status": "ready", "missing": [], "message": "Recommended models configured."}
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
- {
2
- "name": "@allansantos-dev/smart-tool",
3
- "version": "0.9.0",
4
- "description": "Local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search. Windows.",
5
- "license": "Apache-2.0",
6
- "author": "Allan Santos",
7
- "homepage": "https://allansantos-dv.github.io/smart-tool/",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/AllanSantos-DV/smart-tool-dist.git"
11
- },
12
- "keywords": [
13
- "mcp",
14
- "claude-code",
15
- "codex",
16
- "semantic-search",
17
- "web-search",
18
- "coding-agent"
19
- ],
20
- "bin": {
21
- "smart-tool": "bin/smart-tool.mjs"
22
- },
23
- "os": [
24
- "win32"
25
- ],
26
- "cpu": [
27
- "x64"
28
- ],
29
- "engines": {
30
- "node": ">=18"
31
- },
32
- "publishConfig": {
33
- "access": "public"
34
- }
35
- }
1
+ {
2
+ "name": "@allansantos-dev/smart-tool",
3
+ "version": "0.9.2",
4
+ "description": "Local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search. Windows.",
5
+ "license": "Apache-2.0",
6
+ "author": "Allan Santos",
7
+ "homepage": "https://allansantos-dv.github.io/smart-tool/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/AllanSantos-DV/smart-tool-dist.git"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "claude-code",
15
+ "codex",
16
+ "semantic-search",
17
+ "web-search",
18
+ "coding-agent"
19
+ ],
20
+ "bin": {
21
+ "smart-tool": "bin/smart-tool.mjs"
22
+ },
23
+ "os": [
24
+ "win32"
25
+ ],
26
+ "cpu": [
27
+ "x64"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }