@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/CHANGELOG.md CHANGED
@@ -1,23 +1,50 @@
1
- # Changelog
2
-
3
- ## 0.9.0 - beta
4
-
5
- First release for beta testers. Windows only.
6
-
7
- - `smart_search`: semantic + lexical search (Weaviate-style relative score fusion), results split into code, tests and
8
- docs, likeliest functions per chunk, per-project language profile; asks the calling agent to define the scope when no
9
- scope model is configured.
10
- - `web_search`: parallel sources (DuckDuckGo, Yahoo, Wikipedia, Exa, Parallel, Keenable, Google and Startpage through a
11
- resident browser), bilingual queries, cache shared across sessions with volatility checks, research mode.
12
- - `web_fetch`: reads a known URL and returns only the answer to the prompt, 24 h page cache, browser rendering for
13
- JavaScript pages.
14
- - Model gateway through adapters: built-in OpenAI-compatible adapter; user adapters as Python files in
15
- `~/.smart-tool/adapters`.
16
- - Optional local embedder through the mcp-memory sidecar when the gateway has no embedding model.
17
- - PreToolUse hook for Claude Code (HTTP to the daemon) and Codex (thin command), with Redirect, Advise only and Off modes.
18
- - Setup screen: model gateway, models, agents (MCP registration, hook install, hook behavior), web provider keys,
19
- contact for public APIs, local models, start at logon.
20
- - The daemon owns its port: falls back to a free port and rewrites the smart-tool URLs the clients keep.
21
- - Install and update with `npx @allansantos-dev/smart-tool@latest install`: the package brings uv (checksum verified)
22
- and a private Python 3.12 in `~/.smart-tool/runtime`, so only Node.js is required. The tray offers the update command
23
- when npm has a newer version. Product page on GitHub Pages.
1
+ # Changelog
2
+
3
+ ## 0.9.2 - beta
4
+
5
+ - Installer reported success after a broken Camoufox download: camoufox 0.5.6 prints the error of `fetch` and exits 0.
6
+ The installer now catches that error and fails the step (retried 3 times), every request of the download has a
7
+ 60 s read timeout (a stalled download used to wait until the 3-minute idle limit), and the final check requires the
8
+ Camoufox and Chromium executables on disk, so an incomplete browser can never end in "Smart Tool ready".
9
+ - Setup screen: text, model and adapter fields were unstyled boxes (inputs without a type), selects and model fields
10
+ now look like comboboxes; the hook change opens inside the client's card in a scrollable block with Confirm and
11
+ Cancel instead of overflowing the page; "Save behavior" works without registering the MCP first; web search keys
12
+ are set one provider at a time from a provider selector instead of a stack of key fields.
13
+ - Product page: the display font was a family that Google Fonts does not serve (titles fell back to Arial Narrow);
14
+ it now uses Big Shoulders Display. The page shows the current version (a test keeps it in sync with version.py),
15
+ links to the release notes and says what the installer does on failures and older installations. A failed Pages
16
+ deploy is retried once, the run fails unless the live page shows the version, and every release republishes it.
17
+
18
+ ## 0.9.1 - beta
19
+
20
+ - Installer on a regular Windows code page (cp1252): the steps' output was garbled or crashed the step; every child
21
+ process now writes UTF-8.
22
+ - Camoufox download (1.3 GB) looked stuck: its progress is shown every 50 MB, a stalled connection fails after 60 s and
23
+ is retried (3 attempts), and a step is stopped only after a stretch without any output instead of a fixed 10 minutes.
24
+ Steps run with closed stdin, so an unexpected prompt fails at once instead of waiting unseen.
25
+ - An older installation in another folder (found through its running tray or daemon, logon task, Startup script or
26
+ agent hooks; only folders made by the installer) is stopped, its hooks are pointed at the new installation and its
27
+ program folder is removed. Its state folders are left in place.
28
+ - Requirements now state about 2 GB of downloads and 4 GB of disk.
29
+
30
+ ## 0.9.0 - beta
31
+
32
+ First release for beta testers. Windows only.
33
+
34
+ - `smart_search`: semantic + lexical search (Weaviate-style relative score fusion), results split into code, tests and
35
+ docs, likeliest functions per chunk, per-project language profile; asks the calling agent to define the scope when no
36
+ scope model is configured.
37
+ - `web_search`: parallel sources (DuckDuckGo, Yahoo, Wikipedia, Exa, Parallel, Keenable, Google and Startpage through a
38
+ resident browser), bilingual queries, cache shared across sessions with volatility checks, research mode.
39
+ - `web_fetch`: reads a known URL and returns only the answer to the prompt, 24 h page cache, browser rendering for
40
+ JavaScript pages.
41
+ - Model gateway through adapters: built-in OpenAI-compatible adapter; user adapters as Python files in
42
+ `~/.smart-tool/adapters`.
43
+ - Optional local embedder through the mcp-memory sidecar when the gateway has no embedding model.
44
+ - PreToolUse hook for Claude Code (HTTP to the daemon) and Codex (thin command), with Redirect, Advise only and Off modes.
45
+ - Setup screen: model gateway, models, agents (MCP registration, hook install, hook behavior), web provider keys,
46
+ contact for public APIs, local models, start at logon.
47
+ - The daemon owns its port: falls back to a free port and rewrites the smart-tool URLs the clients keep.
48
+ - Install and update with `npx @allansantos-dev/smart-tool@latest install`: the package brings uv (checksum verified)
49
+ and a private Python 3.12 in `~/.smart-tool/runtime`, so only Node.js is required. The tray offers the update command
50
+ when npm has a newer version. Product page on GitHub Pages.
package/NOTICE CHANGED
@@ -1,32 +1,32 @@
1
- Smart Tool
2
- Copyright 2026 Allan Santos
3
-
4
- Licensed under the Apache License, Version 2.0 (see LICENSE).
5
-
6
- Smart Tool does not bundle third-party code. The installer downloads the components below from their official
7
- sources into the installation folder or the user's profile; each keeps its own license.
8
-
9
- Python packages (installation venv, from PyPI)
10
- - ddgs (MIT)
11
- - trafilatura (Apache-2.0)
12
- - pystray (LGPL-3.0; used unmodified as a library)
13
- - Pillow (MIT-CMU)
14
- - PyYAML (MIT)
15
-
16
- Browser runtime (web_adapters/browser/.venv, from PyPI and the projects' download servers)
17
- - camoufox Python package (MIT) and the Camoufox browser, a Firefox fork (MPL-2.0), fetched by `camoufox fetch`
18
- - crawl4ai (Apache-2.0), https://github.com/unclecode/crawl4ai: page rendering is powered by Crawl4AI
19
- - Playwright (Apache-2.0) and the Chromium build it installs (BSD-style, see https://www.chromium.org)
20
-
21
- Node runtime and packages (web_adapters/node, from nodejs.org and npm)
22
- - Node.js, downloaded only when no supported Node is installed (MIT and bundled licenses)
23
- - open-websearch (Apache-2.0)
24
- - typescript (Apache-2.0)
25
- - java-parser (Apache-2.0)
26
- - @angular/compiler (MIT)
27
- - their dependencies as pinned in web_adapters/node/package-lock.json (MIT, ISC, BSD and Apache-2.0)
28
-
29
- Web search sources
30
- Smart Tool queries public search engines and APIs (DuckDuckGo, Yahoo, Wikipedia, Exa, Parallel, Keenable, Google and
31
- Startpage through a local browser, and optionally Tavily and Firecrawl with your own key). Using them is subject to
32
- each service's terms of use; free tiers are rate limited per IP address.
1
+ Smart Tool
2
+ Copyright 2026 Allan Santos
3
+
4
+ Licensed under the Apache License, Version 2.0 (see LICENSE).
5
+
6
+ Smart Tool does not bundle third-party code. The installer downloads the components below from their official
7
+ sources into the installation folder or the user's profile; each keeps its own license.
8
+
9
+ Python packages (installation venv, from PyPI)
10
+ - ddgs (MIT)
11
+ - trafilatura (Apache-2.0)
12
+ - pystray (LGPL-3.0; used unmodified as a library)
13
+ - Pillow (MIT-CMU)
14
+ - PyYAML (MIT)
15
+
16
+ Browser runtime (web_adapters/browser/.venv, from PyPI and the projects' download servers)
17
+ - camoufox Python package (MIT) and the Camoufox browser, a Firefox fork (MPL-2.0), fetched by `camoufox fetch`
18
+ - crawl4ai (Apache-2.0), https://github.com/unclecode/crawl4ai: page rendering is powered by Crawl4AI
19
+ - Playwright (Apache-2.0) and the Chromium build it installs (BSD-style, see https://www.chromium.org)
20
+
21
+ Node runtime and packages (web_adapters/node, from nodejs.org and npm)
22
+ - Node.js, downloaded only when no supported Node is installed (MIT and bundled licenses)
23
+ - open-websearch (Apache-2.0)
24
+ - typescript (Apache-2.0)
25
+ - java-parser (Apache-2.0)
26
+ - @angular/compiler (MIT)
27
+ - their dependencies as pinned in web_adapters/node/package-lock.json (MIT, ISC, BSD and Apache-2.0)
28
+
29
+ Web search sources
30
+ Smart Tool queries public search engines and APIs (DuckDuckGo, Yahoo, Wikipedia, Exa, Parallel, Keenable, Google and
31
+ Startpage through a local browser, and optionally Tavily and Firecrawl with your own key). Using them is subject to
32
+ each service's terms of use; free tiers are rate limited per IP address.
package/README.md CHANGED
@@ -1,116 +1,117 @@
1
- # Smart Tool
2
-
3
- A local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search:
4
-
5
- | Tool | What it does |
6
- |---|---|
7
- | `smart_search` / `smart_search_result` | Semantic + lexical search over an indexed project, results split into code, tests and docs, with the likeliest functions per chunk |
8
- | `web_search` / `web_search_result` | Web search across several sources in parallel, in the query language and in English, with a cache shared across sessions; `depth="research"` runs a multi-round research loop |
9
- | `web_fetch` | Reads a known URL and returns only the answer to your prompt, written by a small model; pages are cached for 24 h |
10
- | `project_manage` | Registers and indexes projects, shows coverage, code maps, duplicated functions and index storage |
11
-
12
- An optional hook routes the agent's native Grep/Glob/Read/Bash/WebSearch/WebFetch calls to these tools when they
13
- are cheaper, or just tells the agent what Smart Tool would do.
14
-
15
- **Status:** 0.9.0 beta. Windows only.
16
-
17
- ## Requirements
18
-
19
- - Windows 10 or 11 (x64) and Node.js 18 or newer for `npx`. Python is not needed: the installer brings its own.
20
- - About 1 GB of downloads on first install (Python 3.12 and its packages, Node.js when no supported version is
21
- installed, the Camoufox and Chromium browsers used for pages that need JavaScript).
22
- - An OpenAI-compatible model API for embeddings and small chat calls (OpenAI, OpenRouter, a LiteLLM proxy, Ollama...).
23
- Web search works without it; smart_search falls back to lexical search.
24
-
25
- ## Install
26
-
27
- ```powershell
28
- npx @allansantos-dev/smart-tool@latest install
29
- ```
30
-
31
- Run the same command to update; `~/.smart-tool` is kept. It downloads [uv](https://docs.astral.sh/uv/) (checksum
32
- verified) and a private Python 3.12 into `~/.smart-tool/runtime`, with nothing added to `PATH` or the registry, then
33
- copies Smart Tool to `%LOCALAPPDATA%\Programs\SmartTool` with its own virtual environment,
34
- installs the web runtime, registers a per-user Scheduled Task that starts the tray at logon and starts the daemon on
35
- `http://127.0.0.1:8765` (another free port is used, and announced, if 8765 is taken).
36
-
37
- State lives in `~/.smart-tool`: `config.json`, your model adapters in `adapters/` and indexes, caches and logs in
38
- `data/`. Secrets (API keys) are encrypted with Windows DPAPI for your user.
39
-
40
- ## Set up
41
-
42
- Open the setup screen from the tray icon ("Open settings") or at `http://127.0.0.1:8765/setup`.
43
-
44
- 1. **Model gateway**: pick an adapter and fill its fields. The built-in **OpenAI-compatible** adapter takes a base
45
- URL ending where the API starts, like the OpenAI SDK `base_url` (`https://api.openai.com/v1`,
46
- `https://openrouter.ai/api/v1`, `http://localhost:11434/v1`), and an optional API key. Saving tests `/models` first.
47
- 2. **Models**: embedding, rerank (optional; without it results use the hybrid order), the router model (small, no
48
- reasoning: it decides before tool calls), the index classification model and the research model. Any id the gateway
49
- accepts works; the catalog is shown as suggestions.
50
- 3. **Agents**: register the MCP server in Claude Code or Codex (runs their official `mcp add` command), install the
51
- hook and choose its behavior:
52
- - **Redirect** (default): the native tool is denied with the reason and the Smart Tool tool to use.
53
- - **Advise only**: the native tool runs; the agent receives the same advice next to the result and decides.
54
- - **Off**: no routing.
55
- 4. **Local models** (optional): if the mcp-memory embedding sidecar is installed on the machine, its local embedder
56
- can serve as a fallback (or preferred) embedding model.
57
- 5. **Web search providers** work without keys on their free public tiers; add your own key for more volume. Set your
58
- own contact (e-mail or URL) if you want public APIs such as Wikimedia to see it in the User-Agent.
59
-
60
- ## Writing a model adapter
61
-
62
- An adapter is a Python file in `~/.smart-tool/adapters/`. It appears in the setup screen next to the built-in one.
63
-
64
- ```python
65
- import json
66
- import urllib.request
67
-
68
- NAME = "my_gateway" # unique id
69
- LABEL = "My gateway" # shown in the setup screen
70
- FIELDS = ( # rendered as inputs; secret values are stored with DPAPI
71
- {"name": "url", "label": "Address", "required": True, "placeholder": "http://10.0.0.5"},
72
- {"name": "token", "label": "Token", "secret": True},
73
- )
74
-
75
-
76
- def request(options, route, body=None, timeout=15):
77
- """route is OpenAI-style: models, model/info, embeddings, chat/completions, rerank. body None means GET.
78
- Return (parsed JSON, response headers); let urllib.error.HTTPError propagate."""
79
- data = json.dumps(body).encode() if body is not None else None
80
- headers = {"Content-Type": "application/json"}
81
- if options.get("token"):
82
- headers["Authorization"] = "Bearer " + options["token"]
83
- req = urllib.request.Request(f"{options['url']}/v1/{route}", data=data, headers=headers)
84
- with urllib.request.urlopen(req, timeout=timeout) as response:
85
- return json.load(response), response.headers
86
-
87
-
88
- def verify(options):
89
- """Raise RuntimeError with a reason a person can act on."""
90
- request(options, "models", timeout=10)
91
- ```
92
-
93
- Embeddings and chat use the OpenAI request and response formats; rerank uses the Cohere/Jina format served by
94
- LiteLLM (`{"model", "query", "documents", "top_n"}` returning `results[].index` and `relevance_score`). A file that
95
- fails to load is listed in the setup screen with its error and does not affect the others.
96
-
97
- ## Data and privacy
98
-
99
- - Indexed code stays on your machine. Chunks are sent to your model gateway to compute embeddings, and small excerpts
100
- go to its chat models (scope, profile, routing, web_fetch answers). Use a local gateway (Ollama, LiteLLM) to keep
101
- everything local.
102
- - Web queries go to the search sources listed in `NOTICE`. Their free tiers are rate limited per IP address; respect
103
- each service's terms of use.
104
- - Logs and metrics are written to `~/.smart-tool/data` and never leave the machine.
105
-
106
- ## Uninstall
107
-
108
- Turn off "Start automatically" in the setup screen, quit from the tray, remove the hook entries from
109
- `~/.claude/settings.json` / `~/.codex/hooks.json` (the setup screen shows them), run `claude mcp remove smart-tool -s
110
- user` / `codex mcp remove smart-tool`, then delete `%LOCALAPPDATA%\Programs\SmartTool` and `~/.smart-tool`.
111
-
112
- ## License
113
-
114
- Apache License 2.0. See `LICENSE` and `NOTICE` for third-party components.
115
-
116
- Page rendering is powered by [Crawl4AI](https://github.com/unclecode/crawl4ai) (Apache-2.0).
1
+ # Smart Tool
2
+
3
+ A local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search:
4
+
5
+ | Tool | What it does |
6
+ |---|---|
7
+ | `smart_search` / `smart_search_result` | Semantic + lexical search over an indexed project, results split into code, tests and docs, with the likeliest functions per chunk |
8
+ | `web_search` / `web_search_result` | Web search across several sources in parallel, in the query language and in English, with a cache shared across sessions; `depth="research"` runs a multi-round research loop |
9
+ | `web_fetch` | Reads a known URL and returns only the answer to your prompt, written by a small model; pages are cached for 24 h |
10
+ | `project_manage` | Registers and indexes projects, shows coverage, code maps, duplicated functions and index storage |
11
+
12
+ An optional hook routes the agent's native Grep/Glob/Read/Bash/WebSearch/WebFetch calls to these tools when they
13
+ are cheaper, or just tells the agent what Smart Tool would do.
14
+
15
+ **Status:** 0.9.0 beta. Windows only.
16
+
17
+ ## Requirements
18
+
19
+ - Windows 10 or 11 (x64) and Node.js 18 or newer for `npx`. Python is not needed: the installer brings its own.
20
+ - About 2 GB of downloads and 4 GB of disk on first install: Python 3.12 and its packages, Node.js when no
21
+ supported version is installed, and the browsers for pages that need JavaScript (Camoufox alone is a 1.3 GB
22
+ download; the installer shows its progress).
23
+ - An OpenAI-compatible model API for embeddings and small chat calls (OpenAI, OpenRouter, a LiteLLM proxy, Ollama...).
24
+ Web search works without it; smart_search falls back to lexical search.
25
+
26
+ ## Install
27
+
28
+ ```powershell
29
+ npx @allansantos-dev/smart-tool@latest install
30
+ ```
31
+
32
+ Run the same command to update; `~/.smart-tool` is kept. It downloads [uv](https://docs.astral.sh/uv/) (checksum
33
+ verified) and a private Python 3.12 into `~/.smart-tool/runtime`, with nothing added to `PATH` or the registry, then
34
+ copies Smart Tool to `%LOCALAPPDATA%\Programs\SmartTool` with its own virtual environment,
35
+ installs the web runtime, registers a per-user Scheduled Task that starts the tray at logon and starts the daemon on
36
+ `http://127.0.0.1:8765` (another free port is used, and announced, if 8765 is taken).
37
+
38
+ State lives in `~/.smart-tool`: `config.json`, your model adapters in `adapters/` and indexes, caches and logs in
39
+ `data/`. Secrets (API keys) are encrypted with Windows DPAPI for your user.
40
+
41
+ ## Set up
42
+
43
+ Open the setup screen from the tray icon ("Open settings") or at `http://127.0.0.1:8765/setup`.
44
+
45
+ 1. **Model gateway**: pick an adapter and fill its fields. The built-in **OpenAI-compatible** adapter takes a base
46
+ URL ending where the API starts, like the OpenAI SDK `base_url` (`https://api.openai.com/v1`,
47
+ `https://openrouter.ai/api/v1`, `http://localhost:11434/v1`), and an optional API key. Saving tests `/models` first.
48
+ 2. **Models**: embedding, rerank (optional; without it results use the hybrid order), the router model (small, no
49
+ reasoning: it decides before tool calls), the index classification model and the research model. Any id the gateway
50
+ accepts works; the catalog is shown as suggestions.
51
+ 3. **Agents**: register the MCP server in Claude Code or Codex (runs their official `mcp add` command), install the
52
+ hook and choose its behavior:
53
+ - **Redirect** (default): the native tool is denied with the reason and the Smart Tool tool to use.
54
+ - **Advise only**: the native tool runs; the agent receives the same advice next to the result and decides.
55
+ - **Off**: no routing.
56
+ 4. **Local models** (optional): if the mcp-memory embedding sidecar is installed on the machine, its local embedder
57
+ can serve as a fallback (or preferred) embedding model.
58
+ 5. **Web search providers** work without keys on their free public tiers; add your own key for more volume. Set your
59
+ own contact (e-mail or URL) if you want public APIs such as Wikimedia to see it in the User-Agent.
60
+
61
+ ## Writing a model adapter
62
+
63
+ An adapter is a Python file in `~/.smart-tool/adapters/`. It appears in the setup screen next to the built-in one.
64
+
65
+ ```python
66
+ import json
67
+ import urllib.request
68
+
69
+ NAME = "my_gateway" # unique id
70
+ LABEL = "My gateway" # shown in the setup screen
71
+ FIELDS = ( # rendered as inputs; secret values are stored with DPAPI
72
+ {"name": "url", "label": "Address", "required": True, "placeholder": "http://10.0.0.5"},
73
+ {"name": "token", "label": "Token", "secret": True},
74
+ )
75
+
76
+
77
+ def request(options, route, body=None, timeout=15):
78
+ """route is OpenAI-style: models, model/info, embeddings, chat/completions, rerank. body None means GET.
79
+ Return (parsed JSON, response headers); let urllib.error.HTTPError propagate."""
80
+ data = json.dumps(body).encode() if body is not None else None
81
+ headers = {"Content-Type": "application/json"}
82
+ if options.get("token"):
83
+ headers["Authorization"] = "Bearer " + options["token"]
84
+ req = urllib.request.Request(f"{options['url']}/v1/{route}", data=data, headers=headers)
85
+ with urllib.request.urlopen(req, timeout=timeout) as response:
86
+ return json.load(response), response.headers
87
+
88
+
89
+ def verify(options):
90
+ """Raise RuntimeError with a reason a person can act on."""
91
+ request(options, "models", timeout=10)
92
+ ```
93
+
94
+ Embeddings and chat use the OpenAI request and response formats; rerank uses the Cohere/Jina format served by
95
+ LiteLLM (`{"model", "query", "documents", "top_n"}` returning `results[].index` and `relevance_score`). A file that
96
+ fails to load is listed in the setup screen with its error and does not affect the others.
97
+
98
+ ## Data and privacy
99
+
100
+ - Indexed code stays on your machine. Chunks are sent to your model gateway to compute embeddings, and small excerpts
101
+ go to its chat models (scope, profile, routing, web_fetch answers). Use a local gateway (Ollama, LiteLLM) to keep
102
+ everything local.
103
+ - Web queries go to the search sources listed in `NOTICE`. Their free tiers are rate limited per IP address; respect
104
+ each service's terms of use.
105
+ - Logs and metrics are written to `~/.smart-tool/data` and never leave the machine.
106
+
107
+ ## Uninstall
108
+
109
+ Turn off "Start automatically" in the setup screen, quit from the tray, remove the hook entries from
110
+ `~/.claude/settings.json` / `~/.codex/hooks.json` (the setup screen shows them), run `claude mcp remove smart-tool -s
111
+ user` / `codex mcp remove smart-tool`, then delete `%LOCALAPPDATA%\Programs\SmartTool` and `~/.smart-tool`.
112
+
113
+ ## License
114
+
115
+ Apache License 2.0. See `LICENSE` and `NOTICE` for third-party components.
116
+
117
+ Page rendering is powered by [Crawl4AI](https://github.com/unclecode/crawl4ai) (Apache-2.0).
package/atomic_io.py CHANGED
@@ -1,94 +1,94 @@
1
- #!/usr/bin/env python3
2
- """Gravação atômica de arquivo pequeno de estado/credencial, compartilhada pelos módulos
3
- do Smart Tool."""
4
- import glob
5
- import os
6
- import time
7
- import uuid
8
-
9
- _REPLACE_ATTEMPTS = 4
10
- _REPLACE_BACKOFF_S = 0.05
11
- TMP_SUFFIX = ".tmp."
12
- ORPHAN_AGE_S = 300
13
-
14
-
15
- def rotate(path, max_bytes):
16
- """Keeps one previous generation (`path.1`) once an append-only log reaches max_bytes. Best effort: a log
17
- locked by another process is rotated on a later append, and the append itself still happens."""
18
- try:
19
- if os.path.getsize(path) >= max_bytes:
20
- os.replace(path, path + ".1")
21
- except OSError:
22
- pass
23
-
24
-
25
- def atomic_replace(tmp_path, final_path):
26
- """`os.replace` com retry curto: no Windows, `MoveFileEx` falha com `PermissionError`
27
- enquanto outro processo mantém o destino aberto — e estes arquivos têm vários leitores
28
- concorrentes (hook do router, tray, configure.py). A janela é de
29
- milissegundos, então insistir resolve; a exceção só sobe na última tentativa."""
30
- for attempt in range(_REPLACE_ATTEMPTS):
31
- try:
32
- os.replace(tmp_path, final_path)
33
- return
34
- except PermissionError:
35
- if attempt == _REPLACE_ATTEMPTS - 1:
36
- raise
37
- time.sleep(_REPLACE_BACKOFF_S)
38
-
39
-
40
- def tmp_path_for(final_path):
41
- """Sufixo único por chamada (não por pid): duas threads/processos podem gravar o mesmo
42
- arquivo quase ao mesmo tempo."""
43
- return final_path + TMP_SUFFIX + uuid.uuid4().hex
44
-
45
-
46
- def sweep_orphans(final_path, max_age_s=ORPHAN_AGE_S):
47
- """Remove tmp de gravação interrompida (kill -9, queda de energia) do mesmo arquivo.
48
- Esses tmp carregam o mesmo conteúdo do destino — credencial, no caso — com nome
49
- aleatório e ninguém mais os apagaria. Best-effort: nunca impede a gravação em curso."""
50
- limite = time.time() - max_age_s
51
- for candidate in glob.glob(glob.escape(final_path) + TMP_SUFFIX + "*"):
52
- try:
53
- if os.path.getmtime(candidate) < limite:
54
- os.remove(candidate)
55
- except OSError:
56
- pass
57
-
58
-
59
- def _write_and_replace(final_path, data, encoding=None):
60
- sweep_orphans(final_path)
61
- tmp_path = tmp_path_for(final_path)
62
- try:
63
- # `O_EXCL` garante tmp novo (nunca aproveita um abandonado, que pode ser de outro
64
- # conteúdo); `0o600` é o pedido de permissão restrita onde o SO honra (no Windows
65
- # só liga o atributo read-only — lá a confidencialidade vem da ACL do diretório).
66
- flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_BINARY", 0)
67
- fd = os.open(tmp_path, flags, 0o600)
68
- mode = "wb" if encoding is None else "w"
69
- extra = {} if encoding is None else {"encoding": encoding, "newline": ""}
70
- with os.fdopen(fd, mode, **extra) as f:
71
- f.write(data)
72
- # `os.replace` só garante que o nome aparece inteiro; sem o flush+fsync uma
73
- # queda de energia pode persistir o rename sem os dados e deixar o destino —
74
- # que era o único lugar com o conteúdo anterior — vazio ou com lixo.
75
- f.flush()
76
- os.fsync(f.fileno())
77
- atomic_replace(tmp_path, final_path)
78
- except Exception:
79
- try:
80
- os.remove(tmp_path)
81
- except OSError:
82
- pass
83
- raise
84
-
85
-
86
- def write_secret_bytes(final_path, data):
87
- """Grava bytes em `final_path` com o tmp restrito, `fsync` dos dados antes do rename
88
- (a durabilidade da entrada de diretório fica com o SO) e replace atômico."""
89
- _write_and_replace(final_path, data)
90
-
91
-
92
- def write_secret_text(final_path, text, encoding="utf-8"):
93
- """Mesmo contrato de `write_secret_bytes`, para conteúdo textual (JSON de token)."""
94
- _write_and_replace(final_path, text, encoding=encoding)
1
+ #!/usr/bin/env python3
2
+ """Gravação atômica de arquivo pequeno de estado/credencial, compartilhada pelos módulos
3
+ do Smart Tool."""
4
+ import glob
5
+ import os
6
+ import time
7
+ import uuid
8
+
9
+ _REPLACE_ATTEMPTS = 4
10
+ _REPLACE_BACKOFF_S = 0.05
11
+ TMP_SUFFIX = ".tmp."
12
+ ORPHAN_AGE_S = 300
13
+
14
+
15
+ def rotate(path, max_bytes):
16
+ """Keeps one previous generation (`path.1`) once an append-only log reaches max_bytes. Best effort: a log
17
+ locked by another process is rotated on a later append, and the append itself still happens."""
18
+ try:
19
+ if os.path.getsize(path) >= max_bytes:
20
+ os.replace(path, path + ".1")
21
+ except OSError:
22
+ pass
23
+
24
+
25
+ def atomic_replace(tmp_path, final_path):
26
+ """`os.replace` com retry curto: no Windows, `MoveFileEx` falha com `PermissionError`
27
+ enquanto outro processo mantém o destino aberto — e estes arquivos têm vários leitores
28
+ concorrentes (hook do router, tray, configure.py). A janela é de
29
+ milissegundos, então insistir resolve; a exceção só sobe na última tentativa."""
30
+ for attempt in range(_REPLACE_ATTEMPTS):
31
+ try:
32
+ os.replace(tmp_path, final_path)
33
+ return
34
+ except PermissionError:
35
+ if attempt == _REPLACE_ATTEMPTS - 1:
36
+ raise
37
+ time.sleep(_REPLACE_BACKOFF_S)
38
+
39
+
40
+ def tmp_path_for(final_path):
41
+ """Sufixo único por chamada (não por pid): duas threads/processos podem gravar o mesmo
42
+ arquivo quase ao mesmo tempo."""
43
+ return final_path + TMP_SUFFIX + uuid.uuid4().hex
44
+
45
+
46
+ def sweep_orphans(final_path, max_age_s=ORPHAN_AGE_S):
47
+ """Remove tmp de gravação interrompida (kill -9, queda de energia) do mesmo arquivo.
48
+ Esses tmp carregam o mesmo conteúdo do destino — credencial, no caso — com nome
49
+ aleatório e ninguém mais os apagaria. Best-effort: nunca impede a gravação em curso."""
50
+ limite = time.time() - max_age_s
51
+ for candidate in glob.glob(glob.escape(final_path) + TMP_SUFFIX + "*"):
52
+ try:
53
+ if os.path.getmtime(candidate) < limite:
54
+ os.remove(candidate)
55
+ except OSError:
56
+ pass
57
+
58
+
59
+ def _write_and_replace(final_path, data, encoding=None):
60
+ sweep_orphans(final_path)
61
+ tmp_path = tmp_path_for(final_path)
62
+ try:
63
+ # `O_EXCL` garante tmp novo (nunca aproveita um abandonado, que pode ser de outro
64
+ # conteúdo); `0o600` é o pedido de permissão restrita onde o SO honra (no Windows
65
+ # só liga o atributo read-only — lá a confidencialidade vem da ACL do diretório).
66
+ flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_BINARY", 0)
67
+ fd = os.open(tmp_path, flags, 0o600)
68
+ mode = "wb" if encoding is None else "w"
69
+ extra = {} if encoding is None else {"encoding": encoding, "newline": ""}
70
+ with os.fdopen(fd, mode, **extra) as f:
71
+ f.write(data)
72
+ # `os.replace` só garante que o nome aparece inteiro; sem o flush+fsync uma
73
+ # queda de energia pode persistir o rename sem os dados e deixar o destino —
74
+ # que era o único lugar com o conteúdo anterior — vazio ou com lixo.
75
+ f.flush()
76
+ os.fsync(f.fileno())
77
+ atomic_replace(tmp_path, final_path)
78
+ except Exception:
79
+ try:
80
+ os.remove(tmp_path)
81
+ except OSError:
82
+ pass
83
+ raise
84
+
85
+
86
+ def write_secret_bytes(final_path, data):
87
+ """Grava bytes em `final_path` com o tmp restrito, `fsync` dos dados antes do rename
88
+ (a durabilidade da entrada de diretório fica com o SO) e replace atômico."""
89
+ _write_and_replace(final_path, data)
90
+
91
+
92
+ def write_secret_text(final_path, text, encoding="utf-8"):
93
+ """Mesmo contrato de `write_secret_bytes`, para conteúdo textual (JSON de token)."""
94
+ _write_and_replace(final_path, text, encoding=encoding)
@@ -74,7 +74,11 @@ async function main() {
74
74
  if (process.platform !== "win32" || process.arch !== "x64") fail(`Smart Tool runs on Windows x64 only (this is ${process.platform}-${process.arch}).`);
75
75
  await ensureUv();
76
76
  const python = ensurePython();
77
- const installer = spawnSync(python, [join(PACKAGE_DIR, "install.py"), "--skip-tests", ...rest], { stdio: "inherit", cwd: PACKAGE_DIR });
77
+ const installer = spawnSync(python, [join(PACKAGE_DIR, "install.py"), "--skip-tests", ...rest], {
78
+ stdio: "inherit",
79
+ cwd: PACKAGE_DIR,
80
+ env: { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" },
81
+ });
78
82
  if (installer.error) fail(`the installer did not start: ${installer.error.message}`);
79
83
  process.exit(installer.status ?? 1);
80
84
  }