howzo 0.1.0__tar.gz
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.
- howzo-0.1.0/LICENSE +21 -0
- howzo-0.1.0/PKG-INFO +165 -0
- howzo-0.1.0/README.md +141 -0
- howzo-0.1.0/pyproject.toml +40 -0
- howzo-0.1.0/setup.cfg +4 -0
- howzo-0.1.0/src/howzo/__init__.py +7 -0
- howzo-0.1.0/src/howzo/__main__.py +7 -0
- howzo-0.1.0/src/howzo/cli.py +48 -0
- howzo-0.1.0/src/howzo/commands.py +180 -0
- howzo-0.1.0/src/howzo/config.py +25 -0
- howzo-0.1.0/src/howzo/db.py +74 -0
- howzo-0.1.0/src/howzo/helptext.py +53 -0
- howzo-0.1.0/src/howzo/match.py +59 -0
- howzo-0.1.0/src/howzo/mcp.py +79 -0
- howzo-0.1.0/src/howzo/proc.py +23 -0
- howzo-0.1.0/src/howzo/render.py +32 -0
- howzo-0.1.0/src/howzo/scan/__init__.py +19 -0
- howzo-0.1.0/src/howzo/scan/brew.py +37 -0
- howzo-0.1.0/src/howzo/scan/common.py +48 -0
- howzo-0.1.0/src/howzo/scan/npm.py +29 -0
- howzo-0.1.0/src/howzo/scan/npx.py +44 -0
- howzo-0.1.0/src/howzo/scan/path.py +28 -0
- howzo-0.1.0/src/howzo/scan/pipx.py +31 -0
- howzo-0.1.0/src/howzo/scan/scripts.py +41 -0
- howzo-0.1.0/src/howzo/scan/system.py +30 -0
- howzo-0.1.0/src/howzo/scan/uv.py +21 -0
- howzo-0.1.0/src/howzo.egg-info/PKG-INFO +165 -0
- howzo-0.1.0/src/howzo.egg-info/SOURCES.txt +38 -0
- howzo-0.1.0/src/howzo.egg-info/dependency_links.txt +1 -0
- howzo-0.1.0/src/howzo.egg-info/entry_points.txt +2 -0
- howzo-0.1.0/src/howzo.egg-info/requires.txt +3 -0
- howzo-0.1.0/src/howzo.egg-info/top_level.txt +1 -0
- howzo-0.1.0/tests/test_cli.py +146 -0
- howzo-0.1.0/tests/test_db.py +42 -0
- howzo-0.1.0/tests/test_helptext.py +41 -0
- howzo-0.1.0/tests/test_match.py +74 -0
- howzo-0.1.0/tests/test_mcp.py +55 -0
- howzo-0.1.0/tests/test_proc.py +29 -0
- howzo-0.1.0/tests/test_render.py +49 -0
- howzo-0.1.0/tests/test_scan.py +178 -0
howzo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sohail
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
howzo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: howzo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Knows your machine: ask 'how do I X' in English, get the installed tool + a runnable command.
|
|
5
|
+
Author: Sohail
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sohailchd/howzo
|
|
8
|
+
Project-URL: Repository, https://github.com/sohailchd/howzo
|
|
9
|
+
Keywords: cli,shell,commands,developer-tools,discovery,mcp
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# howzo
|
|
26
|
+
|
|
27
|
+
**Knows your machine.** Ask "how do I X" in English → get the tool that is actually installed on *this* machine, plus a runnable command.
|
|
28
|
+
|
|
29
|
+
howzo is a free, local, **zero-model** command router. It indexes the tools that are actually installed on your box — brew, npm, pipx, uv, system binaries, your own scripts — and answers plain-English questions by matching against that inventory. No accounts, no API keys, no telemetry, and no network access when answering.
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
Generic command helpers (ShellGPT, mang.sh, Atuin) index a static corpus of popular commands. howzo indexes *your machine* instead — the exact tools, versions, and help text you actually have. If it's installed, howzo knows it; if it isn't, howzo doesn't waste your time suggesting it.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
Requires Python 3.9+. Runtime dependencies: **none** (stdlib only).
|
|
38
|
+
|
|
39
|
+
### macOS / Linux
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pipx install howzo # or: uv tool install howzo
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
macOS also has a Homebrew formula (tap required):
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
brew tap sohailchd/howzo && brew install howzo
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Windows
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
py -m pipx install howzo # or: uv tool install howzo
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### From source
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
git clone https://github.com/sohailchd/howzo.git
|
|
61
|
+
pipx install --editable howzo # or: uv tool install --editable howzo
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### First run
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
howzo scan # one-time: build your machine's inventory (~2-3 min)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Rescans are safe: captured help text, `when_to_use` notes, custom entries, and mined npx packages are preserved.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```console
|
|
75
|
+
$ howzo "how do I rotate a pdf"
|
|
76
|
+
pdfq (pipx, 1.0)
|
|
77
|
+
rotate and convert pdf files
|
|
78
|
+
|
|
79
|
+
$ howzo whatis crwl
|
|
80
|
+
crwl (pipx, 0.3.1)
|
|
81
|
+
(binary of pipx crawl4ai)
|
|
82
|
+
|
|
83
|
+
$ howzo kill a process on port 8080
|
|
84
|
+
lsof (brew, 9.9)
|
|
85
|
+
list open files and network connections
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Commands
|
|
89
|
+
|
|
90
|
+
| Command | What it does |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `howzo <query>` | Ask in plain English (implicit `ask`) |
|
|
93
|
+
| `howzo scan [--deep]` | Rebuild inventory; `--deep` also captures `--help` text for every tool |
|
|
94
|
+
| `howzo ask "query"` | Same as `<query>` |
|
|
95
|
+
| `howzo whatis <tool>` | Reverse lookup: what is this tool for? |
|
|
96
|
+
| `howzo deep <tool>` | Capture `--help`/man for one tool on demand |
|
|
97
|
+
| `howzo add <name> "desc"` | Register a tool the scanner can't see (internal CLIs, aliases) |
|
|
98
|
+
| `howzo list [--source S]` | Browse the inventory |
|
|
99
|
+
| `howzo mcp` | Run as a stdio MCP server |
|
|
100
|
+
| `howzo db` | Show the database path |
|
|
101
|
+
|
|
102
|
+
## What it indexes
|
|
103
|
+
|
|
104
|
+
| Source | What's indexed |
|
|
105
|
+
|---|---|
|
|
106
|
+
| brew | formulae + versions + descriptions (`brew info`) |
|
|
107
|
+
| npm | global packages + their installed binaries |
|
|
108
|
+
| pipx | packages + the binaries they provide (e.g. `crwl` → crawl4ai) |
|
|
109
|
+
| uv | `uv tool` installs |
|
|
110
|
+
| scripts | executables in `~/bin` and `~/.local/bin` (one-liner from the script header) |
|
|
111
|
+
| system (Unix) | `/usr/bin` + `/usr/sbin` + `/usr/local/bin` binaries, described via man pages |
|
|
112
|
+
| path (Windows) | executables found on `PATH` (System32, Program Files, …) |
|
|
113
|
+
| npx | `npx`/`bunx`/`pnpm dlx` packages mined from your shell history (zsh, bash, PowerShell) |
|
|
114
|
+
| custom | anything you add with `howzo add` |
|
|
115
|
+
|
|
116
|
+
A typical machine indexes ~1,200 tools.
|
|
117
|
+
|
|
118
|
+
## How it works
|
|
119
|
+
|
|
120
|
+
- **SQLite + FTS5** at `~/.local/share/howzo/howzo.db` (Windows: `%LOCALAPPDATA%\howzo`), one row per tool: name, source, version, oneliner, when-to-use, help excerpt.
|
|
121
|
+
- **Match = BM25 + word-boundary token-coverage re-rank** in Python. No models, no embeddings — `kill` never matches `skill`, `port` never matches `report`.
|
|
122
|
+
- **~20–30 MB RAM**, and answering is fully offline. The network is only touched while scanning, to fetch package descriptions from npm/PyPI.
|
|
123
|
+
- Set `HOWZO_DB=/some/dir` to relocate the database (also how the test suite isolates itself).
|
|
124
|
+
|
|
125
|
+
## MCP
|
|
126
|
+
|
|
127
|
+
howzo runs as a stdio MCP server exposing `howzo_ask`, `howzo_whatis`, and `howzo_list`:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"howzo": { "command": "howzo", "args": ["mcp"] }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
git clone https://github.com/sohailchd/howzo.git && cd howzo
|
|
141
|
+
uv venv .venv
|
|
142
|
+
VIRTUAL_ENV=$PWD/.venv uv pip install -e ".[dev]" # or: pip install -e ".[dev]"
|
|
143
|
+
pytest
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Layout:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
src/howzo/
|
|
150
|
+
├── cli.py # entry point + dispatch
|
|
151
|
+
├── commands.py # scan / ask / whatis / deep / add / list
|
|
152
|
+
├── config.py # platform constants, DB path (HOWZO_DB override)
|
|
153
|
+
├── proc.py # subprocess helpers (cross-platform)
|
|
154
|
+
├── db.py # SQLite + FTS5 schema, upsert
|
|
155
|
+
├── match.py # tokenization, FTS query, BM25 + coverage re-rank
|
|
156
|
+
├── render.py # output formatting
|
|
157
|
+
├── helptext.py # man pages, --help capture
|
|
158
|
+
├── mcp.py # MCP stdio server
|
|
159
|
+
└── scan/ # one module per source: brew, npm, pipx, uv, scripts, system, path, npx
|
|
160
|
+
tests/ # pytest suite (runs against temp DBs, no network)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT — see [LICENSE](LICENSE).
|
howzo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# howzo
|
|
2
|
+
|
|
3
|
+
**Knows your machine.** Ask "how do I X" in English → get the tool that is actually installed on *this* machine, plus a runnable command.
|
|
4
|
+
|
|
5
|
+
howzo is a free, local, **zero-model** command router. It indexes the tools that are actually installed on your box — brew, npm, pipx, uv, system binaries, your own scripts — and answers plain-English questions by matching against that inventory. No accounts, no API keys, no telemetry, and no network access when answering.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Generic command helpers (ShellGPT, mang.sh, Atuin) index a static corpus of popular commands. howzo indexes *your machine* instead — the exact tools, versions, and help text you actually have. If it's installed, howzo knows it; if it isn't, howzo doesn't waste your time suggesting it.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Requires Python 3.9+. Runtime dependencies: **none** (stdlib only).
|
|
14
|
+
|
|
15
|
+
### macOS / Linux
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pipx install howzo # or: uv tool install howzo
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
macOS also has a Homebrew formula (tap required):
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
brew tap sohailchd/howzo && brew install howzo
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Windows
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
py -m pipx install howzo # or: uv tool install howzo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### From source
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
git clone https://github.com/sohailchd/howzo.git
|
|
37
|
+
pipx install --editable howzo # or: uv tool install --editable howzo
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### First run
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
howzo scan # one-time: build your machine's inventory (~2-3 min)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Rescans are safe: captured help text, `when_to_use` notes, custom entries, and mined npx packages are preserved.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
$ howzo "how do I rotate a pdf"
|
|
52
|
+
pdfq (pipx, 1.0)
|
|
53
|
+
rotate and convert pdf files
|
|
54
|
+
|
|
55
|
+
$ howzo whatis crwl
|
|
56
|
+
crwl (pipx, 0.3.1)
|
|
57
|
+
(binary of pipx crawl4ai)
|
|
58
|
+
|
|
59
|
+
$ howzo kill a process on port 8080
|
|
60
|
+
lsof (brew, 9.9)
|
|
61
|
+
list open files and network connections
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Commands
|
|
65
|
+
|
|
66
|
+
| Command | What it does |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `howzo <query>` | Ask in plain English (implicit `ask`) |
|
|
69
|
+
| `howzo scan [--deep]` | Rebuild inventory; `--deep` also captures `--help` text for every tool |
|
|
70
|
+
| `howzo ask "query"` | Same as `<query>` |
|
|
71
|
+
| `howzo whatis <tool>` | Reverse lookup: what is this tool for? |
|
|
72
|
+
| `howzo deep <tool>` | Capture `--help`/man for one tool on demand |
|
|
73
|
+
| `howzo add <name> "desc"` | Register a tool the scanner can't see (internal CLIs, aliases) |
|
|
74
|
+
| `howzo list [--source S]` | Browse the inventory |
|
|
75
|
+
| `howzo mcp` | Run as a stdio MCP server |
|
|
76
|
+
| `howzo db` | Show the database path |
|
|
77
|
+
|
|
78
|
+
## What it indexes
|
|
79
|
+
|
|
80
|
+
| Source | What's indexed |
|
|
81
|
+
|---|---|
|
|
82
|
+
| brew | formulae + versions + descriptions (`brew info`) |
|
|
83
|
+
| npm | global packages + their installed binaries |
|
|
84
|
+
| pipx | packages + the binaries they provide (e.g. `crwl` → crawl4ai) |
|
|
85
|
+
| uv | `uv tool` installs |
|
|
86
|
+
| scripts | executables in `~/bin` and `~/.local/bin` (one-liner from the script header) |
|
|
87
|
+
| system (Unix) | `/usr/bin` + `/usr/sbin` + `/usr/local/bin` binaries, described via man pages |
|
|
88
|
+
| path (Windows) | executables found on `PATH` (System32, Program Files, …) |
|
|
89
|
+
| npx | `npx`/`bunx`/`pnpm dlx` packages mined from your shell history (zsh, bash, PowerShell) |
|
|
90
|
+
| custom | anything you add with `howzo add` |
|
|
91
|
+
|
|
92
|
+
A typical machine indexes ~1,200 tools.
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
- **SQLite + FTS5** at `~/.local/share/howzo/howzo.db` (Windows: `%LOCALAPPDATA%\howzo`), one row per tool: name, source, version, oneliner, when-to-use, help excerpt.
|
|
97
|
+
- **Match = BM25 + word-boundary token-coverage re-rank** in Python. No models, no embeddings — `kill` never matches `skill`, `port` never matches `report`.
|
|
98
|
+
- **~20–30 MB RAM**, and answering is fully offline. The network is only touched while scanning, to fetch package descriptions from npm/PyPI.
|
|
99
|
+
- Set `HOWZO_DB=/some/dir` to relocate the database (also how the test suite isolates itself).
|
|
100
|
+
|
|
101
|
+
## MCP
|
|
102
|
+
|
|
103
|
+
howzo runs as a stdio MCP server exposing `howzo_ask`, `howzo_whatis`, and `howzo_list`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"howzo": { "command": "howzo", "args": ["mcp"] }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
git clone https://github.com/sohailchd/howzo.git && cd howzo
|
|
117
|
+
uv venv .venv
|
|
118
|
+
VIRTUAL_ENV=$PWD/.venv uv pip install -e ".[dev]" # or: pip install -e ".[dev]"
|
|
119
|
+
pytest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Layout:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
src/howzo/
|
|
126
|
+
├── cli.py # entry point + dispatch
|
|
127
|
+
├── commands.py # scan / ask / whatis / deep / add / list
|
|
128
|
+
├── config.py # platform constants, DB path (HOWZO_DB override)
|
|
129
|
+
├── proc.py # subprocess helpers (cross-platform)
|
|
130
|
+
├── db.py # SQLite + FTS5 schema, upsert
|
|
131
|
+
├── match.py # tokenization, FTS query, BM25 + coverage re-rank
|
|
132
|
+
├── render.py # output formatting
|
|
133
|
+
├── helptext.py # man pages, --help capture
|
|
134
|
+
├── mcp.py # MCP stdio server
|
|
135
|
+
└── scan/ # one module per source: brew, npm, pipx, uv, scripts, system, path, npx
|
|
136
|
+
tests/ # pytest suite (runs against temp DBs, no network)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "howzo"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Knows your machine: ask 'how do I X' in English, get the installed tool + a runnable command."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Sohail" }]
|
|
13
|
+
keywords = ["cli", "shell", "commands", "developer-tools", "discovery", "mcp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: MacOS",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
# Update after the GitHub repo is created
|
|
27
|
+
Homepage = "https://github.com/sohailchd/howzo"
|
|
28
|
+
Repository = "https://github.com/sohailchd/howzo"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
howzo = "howzo.cli:main"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["pytest>=7"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
howzo-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""howzo command-line entry point."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from . import commands
|
|
5
|
+
from .config import db_path
|
|
6
|
+
from .mcp import cmd_mcp
|
|
7
|
+
|
|
8
|
+
USAGE = """howzo - knows your machine. Ask "how do I X" in English; get the installed tool + command.
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
howzo <query> just type your question (same as ask)
|
|
12
|
+
howzo scan [--deep] rebuild inventory (brew/npm/pipx/uv/scripts/system). --deep also captures --help
|
|
13
|
+
howzo ask "query"
|
|
14
|
+
howzo whatis <tool>
|
|
15
|
+
howzo deep <tool> capture --help for one tool
|
|
16
|
+
howzo add <name> "desc" register a tool the scanner can't see (internal CLIs, npx aliases)
|
|
17
|
+
howzo list [--source S]
|
|
18
|
+
howzo mcp run as a stdio MCP server (exposes howzo_ask / howzo_whatis / howzo_list)
|
|
19
|
+
howzo db show database path
|
|
20
|
+
|
|
21
|
+
Stdlib only, zero models, fully local. Works on macOS, Linux, and Windows."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv=None):
|
|
25
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
26
|
+
if not args:
|
|
27
|
+
print(USAGE)
|
|
28
|
+
return 1
|
|
29
|
+
cmd, rest = args[0], args[1:]
|
|
30
|
+
if cmd == "scan":
|
|
31
|
+
return commands.cmd_scan(rest)
|
|
32
|
+
if cmd == "ask":
|
|
33
|
+
return commands.cmd_ask(rest)
|
|
34
|
+
if cmd == "whatis":
|
|
35
|
+
return commands.cmd_whatis(rest)
|
|
36
|
+
if cmd == "deep":
|
|
37
|
+
return commands.cmd_deep(rest)
|
|
38
|
+
if cmd == "add":
|
|
39
|
+
return commands.cmd_add(rest)
|
|
40
|
+
if cmd == "list" and (not rest or rest[0].startswith("-")):
|
|
41
|
+
return commands.cmd_list(rest)
|
|
42
|
+
if cmd == "mcp":
|
|
43
|
+
return cmd_mcp(rest)
|
|
44
|
+
if cmd == "db":
|
|
45
|
+
print(db_path())
|
|
46
|
+
return 0
|
|
47
|
+
# implicit ask: howzo <query> == howzo ask <query>
|
|
48
|
+
return commands.cmd_ask(args)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""CLI command handlers (scan / ask / whatis / deep / add / list)."""
|
|
2
|
+
import os
|
|
3
|
+
import sqlite3
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from . import config
|
|
7
|
+
from .db import db, upsert
|
|
8
|
+
from .helptext import capture_help
|
|
9
|
+
from .match import coverage, fts_query, find_by_name, has_word, query_tokens, rank_rows
|
|
10
|
+
from .render import render_tool
|
|
11
|
+
from .scan import scanners
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cmd_scan(args):
|
|
15
|
+
c = db()
|
|
16
|
+
try:
|
|
17
|
+
# preserve on-demand captured help + enrichments across rescans
|
|
18
|
+
keep = {r["name"]: (r["help_excerpt"], r["help_captured_at"], r["when_to_use"])
|
|
19
|
+
for r in c.execute("SELECT name, help_excerpt, help_captured_at, when_to_use FROM tools")}
|
|
20
|
+
custom_rows = {r["name"]: (r["source"], r["version"], r["path"], r["oneliner"],
|
|
21
|
+
r["when_to_use"], r["help_excerpt"], r["help_captured_at"])
|
|
22
|
+
for r in c.execute("SELECT * FROM tools WHERE source IN ('custom','npx')")}
|
|
23
|
+
c.execute("DELETE FROM tools")
|
|
24
|
+
except sqlite3.DatabaseError:
|
|
25
|
+
print(" warning: db corrupted, rebuilding")
|
|
26
|
+
d = config.db_dir()
|
|
27
|
+
for f in os.listdir(d):
|
|
28
|
+
if f.startswith("howzo.db"):
|
|
29
|
+
os.remove(os.path.join(d, f))
|
|
30
|
+
c = db()
|
|
31
|
+
keep, custom_rows = {}, {}
|
|
32
|
+
deep = "--deep" in args
|
|
33
|
+
t0 = time.time()
|
|
34
|
+
for fn in scanners():
|
|
35
|
+
fn(c)
|
|
36
|
+
# custom tools added via 'howzo add' must survive rescans
|
|
37
|
+
for name, (src, ver, path, one, w, h, h_at) in custom_rows.items():
|
|
38
|
+
c.execute("INSERT INTO tools(name, source, version, path, oneliner, when_to_use, help_excerpt, "
|
|
39
|
+
"help_captured_at, scanned_at) VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(name) DO NOTHING",
|
|
40
|
+
(name, src, ver, path, one, w, h, h_at, time.strftime("%Y-%m-%d")))
|
|
41
|
+
for name, (h, h_at, w) in keep.items():
|
|
42
|
+
c.execute("UPDATE tools SET help_excerpt=?, help_captured_at=?, when_to_use=? WHERE name=?",
|
|
43
|
+
(h, h_at, w, name))
|
|
44
|
+
c.commit()
|
|
45
|
+
total = c.execute("SELECT COUNT(*) FROM tools").fetchone()[0]
|
|
46
|
+
print(f" inventory: {total} tools in {time.time()-t0:.0f}s")
|
|
47
|
+
if deep:
|
|
48
|
+
print(" capturing --help (can take a few minutes)...")
|
|
49
|
+
rows = c.execute("SELECT id, name, path FROM tools").fetchall()
|
|
50
|
+
done = 0
|
|
51
|
+
for tid, name, path in rows:
|
|
52
|
+
h = capture_help(name, path)
|
|
53
|
+
if h:
|
|
54
|
+
c.execute("UPDATE tools SET help_excerpt=?, help_captured_at=? WHERE id=?",
|
|
55
|
+
(h, time.strftime("%Y-%m-%d"), tid))
|
|
56
|
+
done += 1
|
|
57
|
+
c.commit()
|
|
58
|
+
print(f" help captured for {done}/{len(rows)} tools")
|
|
59
|
+
else:
|
|
60
|
+
print(" (tip: 'howzo scan --deep' also captures --help for richer answers;")
|
|
61
|
+
print(" 'howzo deep <tool>' captures help for one tool on demand)")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_ask(args):
|
|
66
|
+
q = " ".join(args).strip()
|
|
67
|
+
if not q:
|
|
68
|
+
print('usage: howzo ask "how do I ..."')
|
|
69
|
+
return 1
|
|
70
|
+
c = db()
|
|
71
|
+
row = find_by_name(c, q)
|
|
72
|
+
if row:
|
|
73
|
+
print(render_tool(row, q))
|
|
74
|
+
return 0
|
|
75
|
+
ftsq = fts_query(q)
|
|
76
|
+
toks = query_tokens(q)
|
|
77
|
+
rows = []
|
|
78
|
+
if ftsq:
|
|
79
|
+
try:
|
|
80
|
+
rows = c.execute(
|
|
81
|
+
"SELECT t.*, bm25(tools_fts) AS score FROM tools_fts f JOIN tools t ON t.id=f.rowid "
|
|
82
|
+
"WHERE tools_fts MATCH ? ORDER BY score LIMIT 12", (ftsq,)).fetchall()
|
|
83
|
+
except sqlite3.OperationalError:
|
|
84
|
+
rows = []
|
|
85
|
+
if not rows and toks:
|
|
86
|
+
# fallback: broad LIKE candidates per token, then word-boundary filter
|
|
87
|
+
cand = {}
|
|
88
|
+
for t in toks:
|
|
89
|
+
like = f"%{t}%"
|
|
90
|
+
for r in c.execute("SELECT * FROM tools WHERE lower(name) LIKE ? OR lower(oneliner) LIKE ? "
|
|
91
|
+
"OR lower(when_to_use) LIKE ? OR lower(help_excerpt) LIKE ? LIMIT 60",
|
|
92
|
+
(like, like, like, like)):
|
|
93
|
+
hay = " ".join(filter(None, [r["name"], r["oneliner"], r["when_to_use"], r["help_excerpt"]]))
|
|
94
|
+
if has_word(hay, t):
|
|
95
|
+
cand[r["id"]] = r
|
|
96
|
+
rows = list(cand.values())
|
|
97
|
+
rows = rank_rows(rows, toks)
|
|
98
|
+
if not rows:
|
|
99
|
+
print(f"no match for: {q}\n (try 'howzo scan --deep' to index --help text)")
|
|
100
|
+
return 1
|
|
101
|
+
for r in rows[:3]:
|
|
102
|
+
print(render_tool(r, q))
|
|
103
|
+
print()
|
|
104
|
+
best_cov = max(coverage(r, toks) for r in rows[:3])
|
|
105
|
+
if best_cov < len(toks):
|
|
106
|
+
print(" (partial match - no tool advertises all terms)")
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def cmd_whatis(args):
|
|
111
|
+
c = db()
|
|
112
|
+
for name in args:
|
|
113
|
+
row = find_by_name(c, name)
|
|
114
|
+
if not row:
|
|
115
|
+
print(f"unknown tool: {name}")
|
|
116
|
+
continue
|
|
117
|
+
t = {k: row[k] for k in row.keys()}
|
|
118
|
+
print(render_tool(t, " ".join(args)))
|
|
119
|
+
if t.get("help_excerpt"):
|
|
120
|
+
print(" help:")
|
|
121
|
+
for l in t["help_excerpt"].splitlines()[:12]:
|
|
122
|
+
print(" ", l)
|
|
123
|
+
print()
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cmd_add(args):
|
|
128
|
+
if len(args) < 2:
|
|
129
|
+
print('usage: howzo add <name> "what it does" (e.g. howzo add my-internal-tool \'syncs staging DB\')')
|
|
130
|
+
return 1
|
|
131
|
+
name = args[0]
|
|
132
|
+
desc = " ".join(args[1:])
|
|
133
|
+
c = db()
|
|
134
|
+
upsert(c, name, "custom", "", "", desc)
|
|
135
|
+
c.commit()
|
|
136
|
+
print(f"added: {name} (source=custom, survives rescans)")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def cmd_deep(args):
|
|
141
|
+
if not args:
|
|
142
|
+
print("usage: howzo deep <tool>")
|
|
143
|
+
return 1
|
|
144
|
+
name = args[0]
|
|
145
|
+
c = db()
|
|
146
|
+
row = c.execute("SELECT * FROM tools WHERE lower(name)=?", (name.lower(),)).fetchone()
|
|
147
|
+
if not row:
|
|
148
|
+
print(f"not in inventory: {name} (run 'howzo scan')")
|
|
149
|
+
return 1
|
|
150
|
+
t = {k: row[k] for k in row.keys()}
|
|
151
|
+
print(f"capturing help for {name}...")
|
|
152
|
+
h = capture_help(name, t.get("path"))
|
|
153
|
+
if not h:
|
|
154
|
+
print(" no help captured (binary not found or silent --help)")
|
|
155
|
+
return 1
|
|
156
|
+
c.execute("UPDATE tools SET help_excerpt=?, help_captured_at=? WHERE id=?",
|
|
157
|
+
(h, time.strftime("%Y-%m-%d"), t["id"]))
|
|
158
|
+
c.commit()
|
|
159
|
+
print(f" ok ({len(h)} chars)")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def cmd_list(args):
|
|
164
|
+
c = db()
|
|
165
|
+
src = None
|
|
166
|
+
if "--source" in args:
|
|
167
|
+
src = args[args.index("--source") + 1]
|
|
168
|
+
q = "SELECT * FROM tools"
|
|
169
|
+
params = ()
|
|
170
|
+
if src:
|
|
171
|
+
q += " WHERE source=?"
|
|
172
|
+
params = (src,)
|
|
173
|
+
q += " ORDER BY source, name"
|
|
174
|
+
rows = c.execute(q, params).fetchall()
|
|
175
|
+
print(f"{'TOOL':<28} {'SRC':<8} {'VER':<14} WHAT")
|
|
176
|
+
for r in rows:
|
|
177
|
+
t = {k: r[k] for k in r.keys()}
|
|
178
|
+
print(f"{t['name']:<28} {t['source']:<8} {str(t.get('version') or '')[:13]:<14} {t.get('oneliner','')[:70]}")
|
|
179
|
+
print(f"\n{len(rows)} tools")
|
|
180
|
+
return 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Platform constants and paths.
|
|
2
|
+
|
|
3
|
+
The database location can be overridden with the HOWZO_DB environment
|
|
4
|
+
variable (the test suite uses this to run against temp directories).
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
IS_WINDOWS = os.name == "nt"
|
|
9
|
+
HOME = os.path.expanduser("~")
|
|
10
|
+
|
|
11
|
+
# Directories scanned for user scripts (see scan/scripts.py)
|
|
12
|
+
SCAN_DIRS = [os.path.join(HOME, "bin"), os.path.join(HOME, ".local", "bin")]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def db_dir():
|
|
16
|
+
override = os.environ.get("HOWZO_DB")
|
|
17
|
+
if override:
|
|
18
|
+
return override
|
|
19
|
+
if IS_WINDOWS:
|
|
20
|
+
return os.path.join(os.environ.get("LOCALAPPDATA") or HOME, "howzo")
|
|
21
|
+
return os.path.join(HOME, ".local", "share", "howzo")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def db_path():
|
|
25
|
+
return os.path.join(db_dir(), "howzo.db")
|