interactive-semantic-hunt 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.
- interactive_semantic_hunt-0.1.0/LICENSE +21 -0
- interactive_semantic_hunt-0.1.0/PKG-INFO +316 -0
- interactive_semantic_hunt-0.1.0/README.md +280 -0
- interactive_semantic_hunt-0.1.0/pyproject.toml +125 -0
- interactive_semantic_hunt-0.1.0/setup.cfg +4 -0
- interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/PKG-INFO +316 -0
- interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/SOURCES.txt +57 -0
- interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/dependency_links.txt +1 -0
- interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/entry_points.txt +4 -0
- interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/requires.txt +13 -0
- interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/top_level.txt +1 -0
- interactive_semantic_hunt-0.1.0/src/ish/__init__.py +6 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/llama_cpp.py +50 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/ollama.py +112 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/prefixes.py +87 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/sentence_transformer.py +47 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/limits.py +90 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/markup.py +93 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/plugins.py +135 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/python.py +127 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/structured.py +234 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/tree_sitter.py +249 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/vcs/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/vcs/git.py +90 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/federated.py +117 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/pure_python.py +157 -0
- interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/sqlite.py +538 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/index.py +227 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/ports/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/ports/embedder.py +31 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/ports/parser.py +39 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/ports/vector_store.py +165 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/preview.py +39 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/scan.py +179 -0
- interactive_semantic_hunt-0.1.0/src/ish/application/search.py +367 -0
- interactive_semantic_hunt-0.1.0/src/ish/bootstrap.py +412 -0
- interactive_semantic_hunt-0.1.0/src/ish/domain/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/domain/chunk.py +30 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/args.py +165 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/complete.py +48 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/log.py +141 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/main.py +168 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/complete.py +115 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/format.py +50 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/mcp/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/mcp/protocol.py +171 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/mcp/server.py +384 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/python/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/python/api.py +195 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/tui/__init__.py +0 -0
- interactive_semantic_hunt-0.1.0/src/ish/interfaces/tui/app.py +393 -0
- interactive_semantic_hunt-0.1.0/src/ish/settings.py +387 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Kristiansen
|
|
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.
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: interactive-semantic-hunt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interactive semantic search for code, inspired by fzf
|
|
5
|
+
Author-email: David Kristiansen <david@kristiansen.tech>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/davetothek/interactive-semantic-hunt
|
|
8
|
+
Project-URL: Repository, https://github.com/davetothek/interactive-semantic-hunt
|
|
9
|
+
Project-URL: Issues, https://github.com/davetothek/interactive-semantic-hunt/issues
|
|
10
|
+
Keywords: search,semantic,embeddings,code,fzf,tui,mcp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=2.5.2
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: textual>=0.70.0
|
|
27
|
+
Requires-Dist: tree-sitter>=0.26.0
|
|
28
|
+
Requires-Dist: tree-sitter-c>=0.24.2
|
|
29
|
+
Requires-Dist: tree-sitter-cpp>=0.23.4
|
|
30
|
+
Provides-Extra: llama
|
|
31
|
+
Requires-Dist: llama-cpp-python>=0.2.75; extra == "llama"
|
|
32
|
+
Requires-Dist: huggingface-hub>=0.23.0; extra == "llama"
|
|
33
|
+
Provides-Extra: st
|
|
34
|
+
Requires-Dist: sentence-transformers; extra == "st"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# ish — Interactive Semantic Hunt
|
|
38
|
+
|
|
39
|
+
Semantic search for code, inspired by `fzf`. Point `ish` at a directory. It parses
|
|
40
|
+
each source file into named chunks, embeds them with a local model, and ranks them
|
|
41
|
+
against your query. Everything runs on your machine.
|
|
42
|
+
|
|
43
|
+
Languages: Python, C and C++, Markdown, and AsciiDoc. Documentation is indexed
|
|
44
|
+
beside the code it describes, so one query searches both.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install interactive-semantic-hunt
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Nothing in that install compiles: the default backend reaches Ollama over
|
|
53
|
+
HTTP with the standard library. A backend that runs the model in this
|
|
54
|
+
process is an extra — `[llama]` for llama.cpp, `[st]` for
|
|
55
|
+
sentence-transformers.
|
|
56
|
+
|
|
57
|
+
To work on ish itself, clone it and run `uv sync`.
|
|
58
|
+
|
|
59
|
+
The default embedding backend is Ollama, which keeps the model resident so no
|
|
60
|
+
run pays a model load. Start it once and pull the embedding model:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
ollama serve
|
|
64
|
+
ollama pull nomic-embed-text
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Set `OLLAMA_HOST` to reach a daemon elsewhere.
|
|
68
|
+
|
|
69
|
+
Two other backends need no daemon:
|
|
70
|
+
|
|
71
|
+
- `--embedder llama.cpp` downloads a GGUF model and loads it per run. Slower per
|
|
72
|
+
query, faster for a first index of a large tree. Needs the `[llama]` extra.
|
|
73
|
+
- `--embedder st` uses sentence-transformers. Needs the `[st]` extra.
|
|
74
|
+
|
|
75
|
+
## Use
|
|
76
|
+
|
|
77
|
+
List every chunk under a path:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
ish src/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
src/ish/domain/chunk.py:7-28 class Chunk
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Search for a query:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
ish "parse a python file" src/
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
[0.71] src/ish/adapters/parser/python.py:14-31 method PythonParser.parse
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run the interactive picker and open the selection in your editor. Type to
|
|
98
|
+
search, `up`/`down` or `ctrl+p`/`ctrl+n` to move, `enter` to choose, `escape`
|
|
99
|
+
to quit. Narrow without leaving the query line:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
state machine transitions every language
|
|
103
|
+
lang:cpp state machine transitions the implementation
|
|
104
|
+
lang:yaml under:/10.System/ state the tests that cover it
|
|
105
|
+
type:doc how do I configure this the prose, not the code
|
|
106
|
+
type:test,doc retry backoff the tests and what they document
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Press Tab to finish a filter word. `ty` becomes `type:`, `lang:cp` becomes
|
|
110
|
+
`lang:cpp`, and `under:/s` becomes `under:/src/`; a word with several answers
|
|
111
|
+
grows as far as they agree and names the rest. `ish-complete` does the work, so
|
|
112
|
+
any picker can call it.
|
|
113
|
+
|
|
114
|
+
`lang:`, `under:`, and `type:` work in the query line of every interface —
|
|
115
|
+
the command line, the picker, Neovim, and MCP. The words are taken out
|
|
116
|
+
before the query is embedded, so the model sees the question rather than
|
|
117
|
+
how it was narrowed.
|
|
118
|
+
|
|
119
|
+
A language may be named however it comes to mind. `c`, `c++`, `cxx`, `h`, and
|
|
120
|
+
`hpp` all mean `cpp`, because one parser reads them all; `adoc` means
|
|
121
|
+
`asciidoc`, `md` means `markdown`, `py` means `python`, and `yml` means `yaml`.
|
|
122
|
+
|
|
123
|
+
`type:` sorts every chunk into exactly one kind. A path decides before a
|
|
124
|
+
language does, so a YAML fixture counts as a test rather than as config:
|
|
125
|
+
|
|
126
|
+
| kind | what it holds |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `code` | Python, C, C++, and anything a plugin parser adds |
|
|
129
|
+
| `doc` | Markdown and AsciiDoc |
|
|
130
|
+
| `test` | anything under `tests/`, `spec/`, `fixtures/`, plus `test_*` and `conftest.py` |
|
|
131
|
+
| `config` | YAML, JSON, and TOML outside a test path |
|
|
132
|
+
|
|
133
|
+
A repository that names its trees its own way can say so, in
|
|
134
|
+
`.ish/config.toml`:
|
|
135
|
+
|
|
136
|
+
```toml
|
|
137
|
+
type_patterns = [
|
|
138
|
+
"test:/[0-9.]*(Tests|Verification)/",
|
|
139
|
+
"doc:/[0-9.]*Specification/",
|
|
140
|
+
]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The first match wins; anything unmatched keeps the reading above.
|
|
144
|
+
|
|
145
|
+
A config beside a subtree adds to the one above it, so a tree settles only
|
|
146
|
+
what it names and inherits the rest. Searching inside an already-indexed tree
|
|
147
|
+
reads that tree's index and narrows the answers to the path, rather than
|
|
148
|
+
starting a second index of the same files.
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
nvim $(ish -i src/)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Options
|
|
155
|
+
|
|
156
|
+
| Flag | Purpose |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `-i`, `--interactive` | Run the TUI picker |
|
|
159
|
+
| `--embedder {llama.cpp,ollama,st}` | Select the embedding backend (default: ollama) |
|
|
160
|
+
| `-v`, `-vv` | Increase log detail |
|
|
161
|
+
| `--color {auto,always,never}` | Control log color |
|
|
162
|
+
| `--limit N` | Maximum search results |
|
|
163
|
+
| `--ignore DIR ...` | Directory names to skip (default `.git .venv venv __pycache__`) |
|
|
164
|
+
| `--include REGEX ...` | Index only paths matching these patterns |
|
|
165
|
+
| `--exclude REGEX ...` | Never index paths matching these patterns |
|
|
166
|
+
| `--git`, `--no-git` | Skip files git ignores (default: on) |
|
|
167
|
+
| `--lang LANG ...` | Show results only from these languages |
|
|
168
|
+
| `--under REGEX` | Show results only from matching paths |
|
|
169
|
+
| `--type TYPE ...` | Show results only of these kinds: `code`, `doc`, `test`, `config` |
|
|
170
|
+
| `--type-patterns TYPE:REGEX ...` | Say what a path holds, overriding the built-in reading |
|
|
171
|
+
| `--model NAME` | Override the backend model |
|
|
172
|
+
| `--refresh` | Bring every stored index at or below the path up to date first |
|
|
173
|
+
| `--reindex` | Discard the stored index and build it again |
|
|
174
|
+
| `--no-cache` | Index in memory only, leaving nothing on disk |
|
|
175
|
+
|
|
176
|
+
Logs go to stderr, so you can pipe stdout safely. A file that cannot be read
|
|
177
|
+
or parsed is counted in one line; `-v` names them.
|
|
178
|
+
|
|
179
|
+
## Use from Neovim
|
|
180
|
+
|
|
181
|
+
`contrib/nvim/ish.lua` is an fzf-lua picker. Copy it to `lua/utils/ish.lua` and
|
|
182
|
+
bind it:
|
|
183
|
+
|
|
184
|
+
```lua
|
|
185
|
+
map('n', '<leader>fi', function() require('utils.ish').search() end,
|
|
186
|
+
{ desc = 'Semantic search (ish)' })
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
It reads `--format grep`, so the built-in previewer opens each result at its
|
|
190
|
+
line, and prints the rank in the leftmost column. `search_lang({'cpp'})`,
|
|
191
|
+
`search_type({'doc'})`, and `search_here()` narrow it, as does a `lang:`,
|
|
192
|
+
`type:`, or `under:` word typed into the query.
|
|
193
|
+
|
|
194
|
+
While the index refreshes, `require('utils.ish').statusline()` renders a bar
|
|
195
|
+
for a statusline — `ish ███░░░░░ 38%` — and an empty string when idle. It reads `vim.g.ish_index_status`, which the picker keeps up to date, and
|
|
196
|
+
shows `ish ✓` briefly when a refresh finishes. Nothing is reported through
|
|
197
|
+
`vim.notify`: with `cmdheight = 0` there is no command line to put a message
|
|
198
|
+
in, so nvim draws one over the last screen row — the statusline itself.
|
|
199
|
+
|
|
200
|
+
The picker never blocks the editor: results are written as they arrive, so
|
|
201
|
+
typing stays smooth however long a search takes.
|
|
202
|
+
|
|
203
|
+
`contrib/nvim/ish_server.lua` keeps one `ish-mcp` process per session. It
|
|
204
|
+
starts on the first search and is reused after that, which cuts a keystroke
|
|
205
|
+
from about 500 ms to about 150 ms. Copy it beside the picker.
|
|
206
|
+
|
|
207
|
+
## Use from Python
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from ish.interfaces.python.api import Ish
|
|
211
|
+
|
|
212
|
+
with Ish("src/") as ish:
|
|
213
|
+
for chunk, score in ish.search("type:doc how to configure", limit=5):
|
|
214
|
+
print(score, chunk.path, chunk.symbol)
|
|
215
|
+
print(ish.status())
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`Ish` holds the index open, so a second query costs a search rather than a
|
|
219
|
+
process start. It offers `search()`, `chunks()`, `index()`, `refresh_all()`,
|
|
220
|
+
and `status()`, and reads `lang:`, `type:`, and `under:` out of the query
|
|
221
|
+
exactly as the other interfaces do.
|
|
222
|
+
|
|
223
|
+
## Use from an agent
|
|
224
|
+
|
|
225
|
+
`ish-mcp` serves the same search over the Model Context Protocol, so an agent
|
|
226
|
+
can query the index directly. Add it to a project with `.mcp.json`:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"mcpServers": {
|
|
231
|
+
"ish": { "command": "uv", "args": ["run", "ish-mcp"] }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
It offers `search_code`, `list_chunks`, `index_status`, and `refresh_index`. The server stays
|
|
237
|
+
resident, so a query costs about 58 ms rather than a process start.
|
|
238
|
+
|
|
239
|
+
A call may narrow one search with `lang`, `under`, `type`, and `limit`, or
|
|
240
|
+
write the same filters into the query text. It cannot change
|
|
241
|
+
what is indexed — those settings come from `ish.toml` only, so no single call can
|
|
242
|
+
shrink an index that another call depends on.
|
|
243
|
+
|
|
244
|
+
## Index
|
|
245
|
+
|
|
246
|
+
The index persists in SQLite under `$XDG_DATA_HOME/ish/`, one file per scanned
|
|
247
|
+
tree. **A search of a parent reads the indexes below it and refreshes none** —
|
|
248
|
+
choosing one of them to write to would be wrong — so it warns and offers
|
|
249
|
+
`--refresh`, which visits each tree in turn. A repeated query reuses it, so only changed files are parsed and only new
|
|
250
|
+
text is embedded. A renamed file re-embeds nothing.
|
|
251
|
+
|
|
252
|
+
Each index records the tree it was built from, so searching a directory also
|
|
253
|
+
searches every index below it. Index the parts of a large project separately and
|
|
254
|
+
search the whole from its root:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
ish "warm" project/docs # index one part
|
|
258
|
+
ish "warm" project/firmware # and another
|
|
259
|
+
ish "how is exposure set" project # searches both
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Searching a parent never rewrites an index below it. Pass `--no-federate` to use
|
|
263
|
+
only the index of the exact path.
|
|
264
|
+
|
|
265
|
+
The index records where each chunk is — its path, line range, kind, and name —
|
|
266
|
+
together with the embedding vector. It does not store the source, so it is not a
|
|
267
|
+
second readable copy of your code. Previews are read from the file, which also
|
|
268
|
+
means they always show the current content.
|
|
269
|
+
|
|
270
|
+
## Configure
|
|
271
|
+
|
|
272
|
+
Every command-line option is also a key in `ish.toml`, under the same name.
|
|
273
|
+
Put project settings in `ish.toml` at the root of your repository:
|
|
274
|
+
|
|
275
|
+
```toml
|
|
276
|
+
embedder = "ollama"
|
|
277
|
+
model = "mxbai-embed-large"
|
|
278
|
+
limit = 10
|
|
279
|
+
ignore = [".git", ".venv", "build", "node_modules"]
|
|
280
|
+
|
|
281
|
+
# Regular expressions, searched against the path.
|
|
282
|
+
exclude = ["/vendor/", "_pb2\\.py$", "(_test|_spec)\\.py$"]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`include` and `exclude` take regular expressions rather than globs, so `/vendor/`
|
|
286
|
+
matches at any depth and alternation works. `exclude` wins over `include`.
|
|
287
|
+
|
|
288
|
+
`--git` is on by default, so anything a `.gitignore` covers stays out of the
|
|
289
|
+
index. Pass `--no-git` to index it anyway.
|
|
290
|
+
|
|
291
|
+
`--lang` and `--under` narrow what a search *returns*. They never change what is
|
|
292
|
+
indexed, so a narrowed query cannot shrink the index:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
ish "how is ranking done" --lang python
|
|
296
|
+
ish "installation steps" --lang markdown asciidoc
|
|
297
|
+
ish "parse a header" --under '/include/'
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
User-level defaults go in `~/.config/ish/ish.toml`. Later sources win:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
defaults < ~/.config/ish/ish.toml < ./ish.toml < ISH_* environment < command line
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Set any option from the environment with the `ISH_` prefix, for example
|
|
307
|
+
`ISH_LIMIT=20` or `ISH_IGNORE=build,dist`.
|
|
308
|
+
|
|
309
|
+
## Develop
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
uv run poe check # lint, typecheck, test
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The architecture is ports and adapters. `spec.md` holds the requirements, and
|
|
316
|
+
`.claude/CLAUDE.md` describes the layers and the composition root.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# ish — Interactive Semantic Hunt
|
|
2
|
+
|
|
3
|
+
Semantic search for code, inspired by `fzf`. Point `ish` at a directory. It parses
|
|
4
|
+
each source file into named chunks, embeds them with a local model, and ranks them
|
|
5
|
+
against your query. Everything runs on your machine.
|
|
6
|
+
|
|
7
|
+
Languages: Python, C and C++, Markdown, and AsciiDoc. Documentation is indexed
|
|
8
|
+
beside the code it describes, so one query searches both.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pip install interactive-semantic-hunt
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Nothing in that install compiles: the default backend reaches Ollama over
|
|
17
|
+
HTTP with the standard library. A backend that runs the model in this
|
|
18
|
+
process is an extra — `[llama]` for llama.cpp, `[st]` for
|
|
19
|
+
sentence-transformers.
|
|
20
|
+
|
|
21
|
+
To work on ish itself, clone it and run `uv sync`.
|
|
22
|
+
|
|
23
|
+
The default embedding backend is Ollama, which keeps the model resident so no
|
|
24
|
+
run pays a model load. Start it once and pull the embedding model:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
ollama serve
|
|
28
|
+
ollama pull nomic-embed-text
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Set `OLLAMA_HOST` to reach a daemon elsewhere.
|
|
32
|
+
|
|
33
|
+
Two other backends need no daemon:
|
|
34
|
+
|
|
35
|
+
- `--embedder llama.cpp` downloads a GGUF model and loads it per run. Slower per
|
|
36
|
+
query, faster for a first index of a large tree. Needs the `[llama]` extra.
|
|
37
|
+
- `--embedder st` uses sentence-transformers. Needs the `[st]` extra.
|
|
38
|
+
|
|
39
|
+
## Use
|
|
40
|
+
|
|
41
|
+
List every chunk under a path:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
ish src/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
src/ish/domain/chunk.py:7-28 class Chunk
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Search for a query:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
ish "parse a python file" src/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
[0.71] src/ish/adapters/parser/python.py:14-31 method PythonParser.parse
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Run the interactive picker and open the selection in your editor. Type to
|
|
62
|
+
search, `up`/`down` or `ctrl+p`/`ctrl+n` to move, `enter` to choose, `escape`
|
|
63
|
+
to quit. Narrow without leaving the query line:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
state machine transitions every language
|
|
67
|
+
lang:cpp state machine transitions the implementation
|
|
68
|
+
lang:yaml under:/10.System/ state the tests that cover it
|
|
69
|
+
type:doc how do I configure this the prose, not the code
|
|
70
|
+
type:test,doc retry backoff the tests and what they document
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Press Tab to finish a filter word. `ty` becomes `type:`, `lang:cp` becomes
|
|
74
|
+
`lang:cpp`, and `under:/s` becomes `under:/src/`; a word with several answers
|
|
75
|
+
grows as far as they agree and names the rest. `ish-complete` does the work, so
|
|
76
|
+
any picker can call it.
|
|
77
|
+
|
|
78
|
+
`lang:`, `under:`, and `type:` work in the query line of every interface —
|
|
79
|
+
the command line, the picker, Neovim, and MCP. The words are taken out
|
|
80
|
+
before the query is embedded, so the model sees the question rather than
|
|
81
|
+
how it was narrowed.
|
|
82
|
+
|
|
83
|
+
A language may be named however it comes to mind. `c`, `c++`, `cxx`, `h`, and
|
|
84
|
+
`hpp` all mean `cpp`, because one parser reads them all; `adoc` means
|
|
85
|
+
`asciidoc`, `md` means `markdown`, `py` means `python`, and `yml` means `yaml`.
|
|
86
|
+
|
|
87
|
+
`type:` sorts every chunk into exactly one kind. A path decides before a
|
|
88
|
+
language does, so a YAML fixture counts as a test rather than as config:
|
|
89
|
+
|
|
90
|
+
| kind | what it holds |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `code` | Python, C, C++, and anything a plugin parser adds |
|
|
93
|
+
| `doc` | Markdown and AsciiDoc |
|
|
94
|
+
| `test` | anything under `tests/`, `spec/`, `fixtures/`, plus `test_*` and `conftest.py` |
|
|
95
|
+
| `config` | YAML, JSON, and TOML outside a test path |
|
|
96
|
+
|
|
97
|
+
A repository that names its trees its own way can say so, in
|
|
98
|
+
`.ish/config.toml`:
|
|
99
|
+
|
|
100
|
+
```toml
|
|
101
|
+
type_patterns = [
|
|
102
|
+
"test:/[0-9.]*(Tests|Verification)/",
|
|
103
|
+
"doc:/[0-9.]*Specification/",
|
|
104
|
+
]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The first match wins; anything unmatched keeps the reading above.
|
|
108
|
+
|
|
109
|
+
A config beside a subtree adds to the one above it, so a tree settles only
|
|
110
|
+
what it names and inherits the rest. Searching inside an already-indexed tree
|
|
111
|
+
reads that tree's index and narrows the answers to the path, rather than
|
|
112
|
+
starting a second index of the same files.
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
nvim $(ish -i src/)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Options
|
|
119
|
+
|
|
120
|
+
| Flag | Purpose |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `-i`, `--interactive` | Run the TUI picker |
|
|
123
|
+
| `--embedder {llama.cpp,ollama,st}` | Select the embedding backend (default: ollama) |
|
|
124
|
+
| `-v`, `-vv` | Increase log detail |
|
|
125
|
+
| `--color {auto,always,never}` | Control log color |
|
|
126
|
+
| `--limit N` | Maximum search results |
|
|
127
|
+
| `--ignore DIR ...` | Directory names to skip (default `.git .venv venv __pycache__`) |
|
|
128
|
+
| `--include REGEX ...` | Index only paths matching these patterns |
|
|
129
|
+
| `--exclude REGEX ...` | Never index paths matching these patterns |
|
|
130
|
+
| `--git`, `--no-git` | Skip files git ignores (default: on) |
|
|
131
|
+
| `--lang LANG ...` | Show results only from these languages |
|
|
132
|
+
| `--under REGEX` | Show results only from matching paths |
|
|
133
|
+
| `--type TYPE ...` | Show results only of these kinds: `code`, `doc`, `test`, `config` |
|
|
134
|
+
| `--type-patterns TYPE:REGEX ...` | Say what a path holds, overriding the built-in reading |
|
|
135
|
+
| `--model NAME` | Override the backend model |
|
|
136
|
+
| `--refresh` | Bring every stored index at or below the path up to date first |
|
|
137
|
+
| `--reindex` | Discard the stored index and build it again |
|
|
138
|
+
| `--no-cache` | Index in memory only, leaving nothing on disk |
|
|
139
|
+
|
|
140
|
+
Logs go to stderr, so you can pipe stdout safely. A file that cannot be read
|
|
141
|
+
or parsed is counted in one line; `-v` names them.
|
|
142
|
+
|
|
143
|
+
## Use from Neovim
|
|
144
|
+
|
|
145
|
+
`contrib/nvim/ish.lua` is an fzf-lua picker. Copy it to `lua/utils/ish.lua` and
|
|
146
|
+
bind it:
|
|
147
|
+
|
|
148
|
+
```lua
|
|
149
|
+
map('n', '<leader>fi', function() require('utils.ish').search() end,
|
|
150
|
+
{ desc = 'Semantic search (ish)' })
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
It reads `--format grep`, so the built-in previewer opens each result at its
|
|
154
|
+
line, and prints the rank in the leftmost column. `search_lang({'cpp'})`,
|
|
155
|
+
`search_type({'doc'})`, and `search_here()` narrow it, as does a `lang:`,
|
|
156
|
+
`type:`, or `under:` word typed into the query.
|
|
157
|
+
|
|
158
|
+
While the index refreshes, `require('utils.ish').statusline()` renders a bar
|
|
159
|
+
for a statusline — `ish ███░░░░░ 38%` — and an empty string when idle. It reads `vim.g.ish_index_status`, which the picker keeps up to date, and
|
|
160
|
+
shows `ish ✓` briefly when a refresh finishes. Nothing is reported through
|
|
161
|
+
`vim.notify`: with `cmdheight = 0` there is no command line to put a message
|
|
162
|
+
in, so nvim draws one over the last screen row — the statusline itself.
|
|
163
|
+
|
|
164
|
+
The picker never blocks the editor: results are written as they arrive, so
|
|
165
|
+
typing stays smooth however long a search takes.
|
|
166
|
+
|
|
167
|
+
`contrib/nvim/ish_server.lua` keeps one `ish-mcp` process per session. It
|
|
168
|
+
starts on the first search and is reused after that, which cuts a keystroke
|
|
169
|
+
from about 500 ms to about 150 ms. Copy it beside the picker.
|
|
170
|
+
|
|
171
|
+
## Use from Python
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from ish.interfaces.python.api import Ish
|
|
175
|
+
|
|
176
|
+
with Ish("src/") as ish:
|
|
177
|
+
for chunk, score in ish.search("type:doc how to configure", limit=5):
|
|
178
|
+
print(score, chunk.path, chunk.symbol)
|
|
179
|
+
print(ish.status())
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`Ish` holds the index open, so a second query costs a search rather than a
|
|
183
|
+
process start. It offers `search()`, `chunks()`, `index()`, `refresh_all()`,
|
|
184
|
+
and `status()`, and reads `lang:`, `type:`, and `under:` out of the query
|
|
185
|
+
exactly as the other interfaces do.
|
|
186
|
+
|
|
187
|
+
## Use from an agent
|
|
188
|
+
|
|
189
|
+
`ish-mcp` serves the same search over the Model Context Protocol, so an agent
|
|
190
|
+
can query the index directly. Add it to a project with `.mcp.json`:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"mcpServers": {
|
|
195
|
+
"ish": { "command": "uv", "args": ["run", "ish-mcp"] }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
It offers `search_code`, `list_chunks`, `index_status`, and `refresh_index`. The server stays
|
|
201
|
+
resident, so a query costs about 58 ms rather than a process start.
|
|
202
|
+
|
|
203
|
+
A call may narrow one search with `lang`, `under`, `type`, and `limit`, or
|
|
204
|
+
write the same filters into the query text. It cannot change
|
|
205
|
+
what is indexed — those settings come from `ish.toml` only, so no single call can
|
|
206
|
+
shrink an index that another call depends on.
|
|
207
|
+
|
|
208
|
+
## Index
|
|
209
|
+
|
|
210
|
+
The index persists in SQLite under `$XDG_DATA_HOME/ish/`, one file per scanned
|
|
211
|
+
tree. **A search of a parent reads the indexes below it and refreshes none** —
|
|
212
|
+
choosing one of them to write to would be wrong — so it warns and offers
|
|
213
|
+
`--refresh`, which visits each tree in turn. A repeated query reuses it, so only changed files are parsed and only new
|
|
214
|
+
text is embedded. A renamed file re-embeds nothing.
|
|
215
|
+
|
|
216
|
+
Each index records the tree it was built from, so searching a directory also
|
|
217
|
+
searches every index below it. Index the parts of a large project separately and
|
|
218
|
+
search the whole from its root:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
ish "warm" project/docs # index one part
|
|
222
|
+
ish "warm" project/firmware # and another
|
|
223
|
+
ish "how is exposure set" project # searches both
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Searching a parent never rewrites an index below it. Pass `--no-federate` to use
|
|
227
|
+
only the index of the exact path.
|
|
228
|
+
|
|
229
|
+
The index records where each chunk is — its path, line range, kind, and name —
|
|
230
|
+
together with the embedding vector. It does not store the source, so it is not a
|
|
231
|
+
second readable copy of your code. Previews are read from the file, which also
|
|
232
|
+
means they always show the current content.
|
|
233
|
+
|
|
234
|
+
## Configure
|
|
235
|
+
|
|
236
|
+
Every command-line option is also a key in `ish.toml`, under the same name.
|
|
237
|
+
Put project settings in `ish.toml` at the root of your repository:
|
|
238
|
+
|
|
239
|
+
```toml
|
|
240
|
+
embedder = "ollama"
|
|
241
|
+
model = "mxbai-embed-large"
|
|
242
|
+
limit = 10
|
|
243
|
+
ignore = [".git", ".venv", "build", "node_modules"]
|
|
244
|
+
|
|
245
|
+
# Regular expressions, searched against the path.
|
|
246
|
+
exclude = ["/vendor/", "_pb2\\.py$", "(_test|_spec)\\.py$"]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`include` and `exclude` take regular expressions rather than globs, so `/vendor/`
|
|
250
|
+
matches at any depth and alternation works. `exclude` wins over `include`.
|
|
251
|
+
|
|
252
|
+
`--git` is on by default, so anything a `.gitignore` covers stays out of the
|
|
253
|
+
index. Pass `--no-git` to index it anyway.
|
|
254
|
+
|
|
255
|
+
`--lang` and `--under` narrow what a search *returns*. They never change what is
|
|
256
|
+
indexed, so a narrowed query cannot shrink the index:
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
ish "how is ranking done" --lang python
|
|
260
|
+
ish "installation steps" --lang markdown asciidoc
|
|
261
|
+
ish "parse a header" --under '/include/'
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
User-level defaults go in `~/.config/ish/ish.toml`. Later sources win:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
defaults < ~/.config/ish/ish.toml < ./ish.toml < ISH_* environment < command line
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Set any option from the environment with the `ISH_` prefix, for example
|
|
271
|
+
`ISH_LIMIT=20` or `ISH_IGNORE=build,dist`.
|
|
272
|
+
|
|
273
|
+
## Develop
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
uv run poe check # lint, typecheck, test
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The architecture is ports and adapters. `spec.md` holds the requirements, and
|
|
280
|
+
`.claude/CLAUDE.md` describes the layers and the composition root.
|