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.
Files changed (59) hide show
  1. interactive_semantic_hunt-0.1.0/LICENSE +21 -0
  2. interactive_semantic_hunt-0.1.0/PKG-INFO +316 -0
  3. interactive_semantic_hunt-0.1.0/README.md +280 -0
  4. interactive_semantic_hunt-0.1.0/pyproject.toml +125 -0
  5. interactive_semantic_hunt-0.1.0/setup.cfg +4 -0
  6. interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/PKG-INFO +316 -0
  7. interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/SOURCES.txt +57 -0
  8. interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/dependency_links.txt +1 -0
  9. interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/entry_points.txt +4 -0
  10. interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/requires.txt +13 -0
  11. interactive_semantic_hunt-0.1.0/src/interactive_semantic_hunt.egg-info/top_level.txt +1 -0
  12. interactive_semantic_hunt-0.1.0/src/ish/__init__.py +6 -0
  13. interactive_semantic_hunt-0.1.0/src/ish/adapters/__init__.py +0 -0
  14. interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/__init__.py +0 -0
  15. interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/llama_cpp.py +50 -0
  16. interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/ollama.py +112 -0
  17. interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/prefixes.py +87 -0
  18. interactive_semantic_hunt-0.1.0/src/ish/adapters/embedder/sentence_transformer.py +47 -0
  19. interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/__init__.py +0 -0
  20. interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/limits.py +90 -0
  21. interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/markup.py +93 -0
  22. interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/plugins.py +135 -0
  23. interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/python.py +127 -0
  24. interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/structured.py +234 -0
  25. interactive_semantic_hunt-0.1.0/src/ish/adapters/parser/tree_sitter.py +249 -0
  26. interactive_semantic_hunt-0.1.0/src/ish/adapters/vcs/__init__.py +0 -0
  27. interactive_semantic_hunt-0.1.0/src/ish/adapters/vcs/git.py +90 -0
  28. interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/__init__.py +0 -0
  29. interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/federated.py +117 -0
  30. interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/pure_python.py +157 -0
  31. interactive_semantic_hunt-0.1.0/src/ish/adapters/vector_store/sqlite.py +538 -0
  32. interactive_semantic_hunt-0.1.0/src/ish/application/__init__.py +0 -0
  33. interactive_semantic_hunt-0.1.0/src/ish/application/index.py +227 -0
  34. interactive_semantic_hunt-0.1.0/src/ish/application/ports/__init__.py +0 -0
  35. interactive_semantic_hunt-0.1.0/src/ish/application/ports/embedder.py +31 -0
  36. interactive_semantic_hunt-0.1.0/src/ish/application/ports/parser.py +39 -0
  37. interactive_semantic_hunt-0.1.0/src/ish/application/ports/vector_store.py +165 -0
  38. interactive_semantic_hunt-0.1.0/src/ish/application/preview.py +39 -0
  39. interactive_semantic_hunt-0.1.0/src/ish/application/scan.py +179 -0
  40. interactive_semantic_hunt-0.1.0/src/ish/application/search.py +367 -0
  41. interactive_semantic_hunt-0.1.0/src/ish/bootstrap.py +412 -0
  42. interactive_semantic_hunt-0.1.0/src/ish/domain/__init__.py +0 -0
  43. interactive_semantic_hunt-0.1.0/src/ish/domain/chunk.py +30 -0
  44. interactive_semantic_hunt-0.1.0/src/ish/interfaces/__init__.py +0 -0
  45. interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/__init__.py +0 -0
  46. interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/args.py +165 -0
  47. interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/complete.py +48 -0
  48. interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/log.py +141 -0
  49. interactive_semantic_hunt-0.1.0/src/ish/interfaces/cli/main.py +168 -0
  50. interactive_semantic_hunt-0.1.0/src/ish/interfaces/complete.py +115 -0
  51. interactive_semantic_hunt-0.1.0/src/ish/interfaces/format.py +50 -0
  52. interactive_semantic_hunt-0.1.0/src/ish/interfaces/mcp/__init__.py +0 -0
  53. interactive_semantic_hunt-0.1.0/src/ish/interfaces/mcp/protocol.py +171 -0
  54. interactive_semantic_hunt-0.1.0/src/ish/interfaces/mcp/server.py +384 -0
  55. interactive_semantic_hunt-0.1.0/src/ish/interfaces/python/__init__.py +0 -0
  56. interactive_semantic_hunt-0.1.0/src/ish/interfaces/python/api.py +195 -0
  57. interactive_semantic_hunt-0.1.0/src/ish/interfaces/tui/__init__.py +0 -0
  58. interactive_semantic_hunt-0.1.0/src/ish/interfaces/tui/app.py +393 -0
  59. 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.