vexor 0.2.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- vexor/__init__.py +1 -1
- vexor/cache.py +299 -26
- vexor/cli.py +340 -193
- vexor/config.py +45 -1
- vexor/modes.py +81 -0
- vexor/providers/__init__.py +3 -0
- vexor/providers/gemini.py +74 -0
- vexor/providers/openai.py +69 -0
- vexor/search.py +38 -69
- vexor/services/__init__.py +9 -0
- vexor/services/cache_service.py +39 -0
- vexor/services/config_service.py +83 -0
- vexor/services/content_extract_service.py +188 -0
- vexor/services/index_service.py +260 -0
- vexor/services/search_service.py +95 -0
- vexor/services/system_service.py +81 -0
- vexor/text.py +53 -10
- vexor/utils.py +24 -9
- vexor-0.5.0.dist-info/METADATA +139 -0
- vexor-0.5.0.dist-info/RECORD +24 -0
- vexor-0.2.0.dist-info/METADATA +0 -102
- vexor-0.2.0.dist-info/RECORD +0 -13
- {vexor-0.2.0.dist-info → vexor-0.5.0.dist-info}/WHEEL +0 -0
- {vexor-0.2.0.dist-info → vexor-0.5.0.dist-info}/entry_points.txt +0 -0
- {vexor-0.2.0.dist-info → vexor-0.5.0.dist-info}/licenses/LICENSE +0 -0
vexor/cli.py
CHANGED
|
@@ -2,27 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import Sequence
|
|
8
|
-
import shutil
|
|
9
|
-
import re
|
|
8
|
+
from typing import Sequence, TYPE_CHECKING
|
|
10
9
|
|
|
11
10
|
import typer
|
|
12
11
|
from rich.console import Console
|
|
13
12
|
from rich.table import Table
|
|
14
13
|
|
|
15
|
-
from . import __version__
|
|
14
|
+
from . import __version__, config as config_module
|
|
15
|
+
from .cache import clear_all_cache, list_cache_entries
|
|
16
16
|
from .config import (
|
|
17
17
|
DEFAULT_BATCH_SIZE,
|
|
18
18
|
DEFAULT_MODEL,
|
|
19
|
+
DEFAULT_PROVIDER,
|
|
20
|
+
SUPPORTED_PROVIDERS,
|
|
19
21
|
load_config,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
)
|
|
23
|
+
from .modes import available_modes, get_strategy
|
|
24
|
+
from .services.cache_service import load_index_metadata_safe
|
|
25
|
+
from .services.config_service import apply_config_updates, get_config_snapshot
|
|
26
|
+
from .services.index_service import IndexStatus, build_index, clear_index_entries
|
|
27
|
+
from .services.search_service import SearchRequest, perform_search
|
|
28
|
+
from .services.system_service import (
|
|
29
|
+
fetch_remote_version,
|
|
30
|
+
find_command_on_path,
|
|
31
|
+
resolve_editor_command,
|
|
32
|
+
version_tuple,
|
|
23
33
|
)
|
|
24
34
|
from .text import Messages, Styles
|
|
25
|
-
from .utils import
|
|
35
|
+
from .utils import resolve_directory, format_path, ensure_positive
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
38
|
+
from .search import SearchResult
|
|
26
39
|
|
|
27
40
|
REMOTE_VERSION_URL = "https://raw.githubusercontent.com/scarletkc/vexor/refs/heads/main/vexor/__init__.py"
|
|
28
41
|
PROJECT_URL = "https://github.com/scarletkc/vexor"
|
|
@@ -36,11 +49,6 @@ app = typer.Typer(
|
|
|
36
49
|
)
|
|
37
50
|
|
|
38
51
|
|
|
39
|
-
@dataclass(slots=True)
|
|
40
|
-
class DisplayResult:
|
|
41
|
-
path: Path
|
|
42
|
-
score: float
|
|
43
|
-
|
|
44
52
|
|
|
45
53
|
def _version_callback(value: bool) -> None:
|
|
46
54
|
if value:
|
|
@@ -48,6 +56,17 @@ def _version_callback(value: bool) -> None:
|
|
|
48
56
|
raise typer.Exit()
|
|
49
57
|
|
|
50
58
|
|
|
59
|
+
def _validate_mode(mode: str) -> str:
|
|
60
|
+
try:
|
|
61
|
+
get_strategy(mode)
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
allowed = ", ".join(available_modes())
|
|
64
|
+
raise typer.BadParameter(
|
|
65
|
+
Messages.ERROR_MODE_INVALID.format(value=mode, allowed=allowed)
|
|
66
|
+
) from exc
|
|
67
|
+
return mode
|
|
68
|
+
|
|
69
|
+
|
|
51
70
|
@app.callback()
|
|
52
71
|
def main(
|
|
53
72
|
version: bool = typer.Option(
|
|
@@ -78,11 +97,26 @@ def search(
|
|
|
78
97
|
"--include-hidden",
|
|
79
98
|
help=Messages.HELP_INCLUDE_HIDDEN,
|
|
80
99
|
),
|
|
100
|
+
mode: str = typer.Option(
|
|
101
|
+
...,
|
|
102
|
+
"--mode",
|
|
103
|
+
"-m",
|
|
104
|
+
help=Messages.HELP_MODE,
|
|
105
|
+
),
|
|
106
|
+
no_recursive: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--no-recursive",
|
|
109
|
+
"-n",
|
|
110
|
+
help=Messages.HELP_RECURSIVE,
|
|
111
|
+
),
|
|
81
112
|
) -> None:
|
|
82
113
|
"""Run the semantic search using a cached index."""
|
|
83
114
|
config = load_config()
|
|
84
115
|
model_name = config.model or DEFAULT_MODEL
|
|
85
116
|
batch_size = config.batch_size if config.batch_size is not None else DEFAULT_BATCH_SIZE
|
|
117
|
+
provider = config.provider or DEFAULT_PROVIDER
|
|
118
|
+
base_url = config.base_url
|
|
119
|
+
api_key = config.api_key
|
|
86
120
|
|
|
87
121
|
clean_query = query.strip()
|
|
88
122
|
if not clean_query:
|
|
@@ -94,45 +128,45 @@ def search(
|
|
|
94
128
|
raise typer.BadParameter(str(exc), param_name="top") from exc
|
|
95
129
|
|
|
96
130
|
directory = resolve_directory(path)
|
|
131
|
+
mode_value = _validate_mode(mode)
|
|
132
|
+
recursive = not no_recursive
|
|
97
133
|
console.print(_styled(Messages.INFO_SEARCH_RUNNING.format(path=directory), Styles.INFO))
|
|
134
|
+
request = SearchRequest(
|
|
135
|
+
query=clean_query,
|
|
136
|
+
directory=directory,
|
|
137
|
+
include_hidden=include_hidden,
|
|
138
|
+
mode=mode_value,
|
|
139
|
+
recursive=recursive,
|
|
140
|
+
top_k=top,
|
|
141
|
+
model_name=model_name,
|
|
142
|
+
batch_size=batch_size,
|
|
143
|
+
provider=provider,
|
|
144
|
+
base_url=base_url,
|
|
145
|
+
api_key=api_key,
|
|
146
|
+
)
|
|
98
147
|
try:
|
|
99
|
-
|
|
148
|
+
response = perform_search(request)
|
|
100
149
|
except FileNotFoundError:
|
|
101
150
|
console.print(
|
|
102
151
|
_styled(Messages.ERROR_INDEX_MISSING.format(path=directory), Styles.ERROR)
|
|
103
152
|
)
|
|
104
153
|
raise typer.Exit(code=1)
|
|
105
|
-
|
|
106
|
-
_warn_if_stale(directory, include_hidden, meta.get("files", []))
|
|
107
|
-
|
|
108
|
-
if not cached_paths:
|
|
109
|
-
console.print(_styled(Messages.INFO_INDEX_EMPTY, Styles.WARNING))
|
|
110
|
-
raise typer.Exit(code=0)
|
|
111
|
-
|
|
112
|
-
searcher = _create_searcher(model_name=model_name, batch_size=batch_size)
|
|
113
|
-
try:
|
|
114
|
-
query_vector = searcher.embed_texts([clean_query])[0]
|
|
115
154
|
except RuntimeError as exc:
|
|
116
155
|
console.print(_styled(str(exc), Styles.ERROR))
|
|
117
156
|
raise typer.Exit(code=1)
|
|
118
157
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
]
|
|
128
|
-
scored.sort(key=lambda item: item.score, reverse=True)
|
|
129
|
-
results = scored[:top]
|
|
130
|
-
|
|
131
|
-
if not results:
|
|
158
|
+
if response.index_empty:
|
|
159
|
+
console.print(_styled(Messages.INFO_INDEX_EMPTY, Styles.WARNING))
|
|
160
|
+
raise typer.Exit(code=0)
|
|
161
|
+
if response.is_stale:
|
|
162
|
+
console.print(
|
|
163
|
+
_styled(Messages.WARNING_INDEX_STALE.format(path=directory), Styles.WARNING)
|
|
164
|
+
)
|
|
165
|
+
if not response.results:
|
|
132
166
|
console.print(_styled(Messages.INFO_NO_RESULTS, Styles.WARNING))
|
|
133
167
|
raise typer.Exit(code=0)
|
|
134
168
|
|
|
135
|
-
_render_results(results,
|
|
169
|
+
_render_results(response.results, response.base_path, response.backend)
|
|
136
170
|
|
|
137
171
|
|
|
138
172
|
@app.command()
|
|
@@ -148,20 +182,84 @@ def index(
|
|
|
148
182
|
"--include-hidden",
|
|
149
183
|
help=Messages.HELP_INDEX_INCLUDE,
|
|
150
184
|
),
|
|
185
|
+
mode: str = typer.Option(
|
|
186
|
+
...,
|
|
187
|
+
"--mode",
|
|
188
|
+
"-m",
|
|
189
|
+
help=Messages.HELP_MODE,
|
|
190
|
+
),
|
|
191
|
+
no_recursive: bool = typer.Option(
|
|
192
|
+
False,
|
|
193
|
+
"--no-recursive",
|
|
194
|
+
"-n",
|
|
195
|
+
help=Messages.HELP_RECURSIVE,
|
|
196
|
+
),
|
|
151
197
|
clear: bool = typer.Option(
|
|
152
198
|
False,
|
|
153
199
|
"--clear",
|
|
154
200
|
help=Messages.HELP_INDEX_CLEAR,
|
|
155
201
|
),
|
|
202
|
+
show_cache: bool = typer.Option(
|
|
203
|
+
False,
|
|
204
|
+
"--show",
|
|
205
|
+
help=Messages.HELP_INDEX_SHOW,
|
|
206
|
+
),
|
|
156
207
|
) -> None:
|
|
157
208
|
"""Create or refresh the cached index for the given directory."""
|
|
158
209
|
config = load_config()
|
|
159
210
|
model_name = config.model or DEFAULT_MODEL
|
|
160
211
|
batch_size = config.batch_size if config.batch_size is not None else DEFAULT_BATCH_SIZE
|
|
212
|
+
provider = config.provider or DEFAULT_PROVIDER
|
|
213
|
+
base_url = config.base_url
|
|
214
|
+
api_key = config.api_key
|
|
161
215
|
|
|
162
216
|
directory = resolve_directory(path)
|
|
217
|
+
mode_value = _validate_mode(mode)
|
|
218
|
+
recursive = not no_recursive
|
|
219
|
+
if clear and show_cache:
|
|
220
|
+
raise typer.BadParameter(Messages.ERROR_INDEX_SHOW_CONFLICT)
|
|
221
|
+
|
|
222
|
+
if show_cache:
|
|
223
|
+
metadata = load_index_metadata_safe(
|
|
224
|
+
directory,
|
|
225
|
+
model_name,
|
|
226
|
+
include_hidden,
|
|
227
|
+
mode_value,
|
|
228
|
+
recursive,
|
|
229
|
+
)
|
|
230
|
+
if not metadata:
|
|
231
|
+
console.print(
|
|
232
|
+
_styled(
|
|
233
|
+
Messages.INFO_INDEX_CLEAR_NONE.format(path=directory),
|
|
234
|
+
Styles.INFO,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
files = metadata.get("files", [])
|
|
240
|
+
console.print(
|
|
241
|
+
_styled(Messages.INFO_INDEX_SHOW_HEADER.format(path=directory), Styles.TITLE)
|
|
242
|
+
)
|
|
243
|
+
summary = Messages.INFO_INDEX_SHOW_SUMMARY.format(
|
|
244
|
+
mode=metadata.get("mode"),
|
|
245
|
+
model=metadata.get("model"),
|
|
246
|
+
hidden="yes" if metadata.get("include_hidden") else "no",
|
|
247
|
+
recursive="yes" if metadata.get("recursive") else "no",
|
|
248
|
+
files=len(files),
|
|
249
|
+
dimension=metadata.get("dimension"),
|
|
250
|
+
version=metadata.get("version"),
|
|
251
|
+
generated=metadata.get("generated_at"),
|
|
252
|
+
)
|
|
253
|
+
console.print(_styled(summary, Styles.INFO))
|
|
254
|
+
return
|
|
255
|
+
|
|
163
256
|
if clear:
|
|
164
|
-
removed =
|
|
257
|
+
removed = clear_index_entries(
|
|
258
|
+
directory,
|
|
259
|
+
include_hidden=include_hidden,
|
|
260
|
+
mode=mode_value,
|
|
261
|
+
recursive=recursive,
|
|
262
|
+
)
|
|
165
263
|
if removed:
|
|
166
264
|
plural = "ies" if removed > 1 else "y"
|
|
167
265
|
console.print(
|
|
@@ -184,34 +282,31 @@ def index(
|
|
|
184
282
|
return
|
|
185
283
|
|
|
186
284
|
console.print(_styled(Messages.INFO_INDEX_RUNNING.format(path=directory), Styles.INFO))
|
|
187
|
-
|
|
188
|
-
|
|
285
|
+
try:
|
|
286
|
+
result = build_index(
|
|
287
|
+
directory,
|
|
288
|
+
include_hidden=include_hidden,
|
|
289
|
+
mode=mode_value,
|
|
290
|
+
recursive=recursive,
|
|
291
|
+
model_name=model_name,
|
|
292
|
+
batch_size=batch_size,
|
|
293
|
+
provider=provider,
|
|
294
|
+
base_url=base_url,
|
|
295
|
+
api_key=api_key,
|
|
296
|
+
)
|
|
297
|
+
except RuntimeError as exc:
|
|
298
|
+
console.print(_styled(str(exc), Styles.ERROR))
|
|
299
|
+
raise typer.Exit(code=1)
|
|
300
|
+
if result.status == IndexStatus.EMPTY:
|
|
189
301
|
console.print(_styled(Messages.INFO_NO_FILES, Styles.WARNING))
|
|
190
302
|
raise typer.Exit(code=0)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
console.print(
|
|
199
|
-
_styled(Messages.INFO_INDEX_UP_TO_DATE.format(path=directory), Styles.INFO)
|
|
200
|
-
)
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
searcher = _create_searcher(model_name=model_name, batch_size=batch_size)
|
|
204
|
-
file_labels = [_label_for_path(file) for file in files]
|
|
205
|
-
embeddings = searcher.embed_texts(file_labels)
|
|
206
|
-
|
|
207
|
-
cache_path = _store_index(
|
|
208
|
-
root=directory,
|
|
209
|
-
model=model_name,
|
|
210
|
-
include_hidden=include_hidden,
|
|
211
|
-
files=files,
|
|
212
|
-
embeddings=embeddings,
|
|
213
|
-
)
|
|
214
|
-
console.print(_styled(Messages.INFO_INDEX_SAVED.format(path=cache_path), Styles.SUCCESS))
|
|
303
|
+
if result.status == IndexStatus.UP_TO_DATE:
|
|
304
|
+
console.print(
|
|
305
|
+
_styled(Messages.INFO_INDEX_UP_TO_DATE.format(path=directory), Styles.INFO)
|
|
306
|
+
)
|
|
307
|
+
return
|
|
308
|
+
if result.cache_path is not None:
|
|
309
|
+
console.print(_styled(Messages.INFO_INDEX_SAVED.format(path=result.cache_path), Styles.SUCCESS))
|
|
215
310
|
|
|
216
311
|
|
|
217
312
|
@app.command()
|
|
@@ -236,57 +331,158 @@ def config(
|
|
|
236
331
|
"--set-batch-size",
|
|
237
332
|
help=Messages.HELP_SET_BATCH,
|
|
238
333
|
),
|
|
334
|
+
set_provider_option: str | None = typer.Option(
|
|
335
|
+
None,
|
|
336
|
+
"--set-provider",
|
|
337
|
+
help=Messages.HELP_SET_PROVIDER,
|
|
338
|
+
),
|
|
339
|
+
set_base_url_option: str | None = typer.Option(
|
|
340
|
+
None,
|
|
341
|
+
"--set-base-url",
|
|
342
|
+
help=Messages.HELP_SET_BASE_URL,
|
|
343
|
+
),
|
|
344
|
+
clear_base_url: bool = typer.Option(
|
|
345
|
+
False,
|
|
346
|
+
"--clear-base-url",
|
|
347
|
+
help=Messages.HELP_CLEAR_BASE_URL,
|
|
348
|
+
),
|
|
239
349
|
show: bool = typer.Option(
|
|
240
350
|
False,
|
|
241
351
|
"--show",
|
|
242
352
|
help=Messages.HELP_SHOW_CONFIG,
|
|
243
353
|
),
|
|
354
|
+
show_index_all: bool = typer.Option(
|
|
355
|
+
False,
|
|
356
|
+
"--show-index-all",
|
|
357
|
+
help=Messages.HELP_SHOW_INDEX_ALL,
|
|
358
|
+
),
|
|
359
|
+
clear_index_all: bool = typer.Option(
|
|
360
|
+
False,
|
|
361
|
+
"--clear-index-all",
|
|
362
|
+
help=Messages.HELP_CLEAR_INDEX_ALL,
|
|
363
|
+
),
|
|
244
364
|
) -> None:
|
|
245
365
|
"""Manage Vexor configuration stored in ~/.vexor/config.json."""
|
|
246
|
-
|
|
366
|
+
if set_batch_option is not None and set_batch_option < 0:
|
|
367
|
+
raise typer.BadParameter(Messages.ERROR_BATCH_NEGATIVE)
|
|
368
|
+
if set_base_url_option and clear_base_url:
|
|
369
|
+
raise typer.BadParameter(Messages.ERROR_BASE_URL_CONFLICT)
|
|
370
|
+
if set_provider_option is not None:
|
|
371
|
+
normalized_provider = set_provider_option.strip().lower()
|
|
372
|
+
if normalized_provider not in SUPPORTED_PROVIDERS:
|
|
373
|
+
allowed = ", ".join(SUPPORTED_PROVIDERS)
|
|
374
|
+
raise typer.BadParameter(
|
|
375
|
+
Messages.ERROR_PROVIDER_INVALID.format(
|
|
376
|
+
value=set_provider_option, allowed=allowed
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
set_provider_option = normalized_provider
|
|
380
|
+
|
|
381
|
+
updates = apply_config_updates(
|
|
382
|
+
api_key=set_api_key_option,
|
|
383
|
+
clear_api_key=clear_api_key,
|
|
384
|
+
model=set_model_option,
|
|
385
|
+
batch_size=set_batch_option,
|
|
386
|
+
provider=set_provider_option,
|
|
387
|
+
base_url=set_base_url_option,
|
|
388
|
+
clear_base_url=clear_base_url,
|
|
389
|
+
)
|
|
247
390
|
|
|
248
|
-
if
|
|
249
|
-
set_api_key(set_api_key_option)
|
|
391
|
+
if updates.api_key_set:
|
|
250
392
|
console.print(_styled(Messages.INFO_API_SAVED, Styles.SUCCESS))
|
|
251
|
-
|
|
252
|
-
if clear_api_key:
|
|
253
|
-
set_api_key(None)
|
|
393
|
+
if updates.api_key_cleared:
|
|
254
394
|
console.print(_styled(Messages.INFO_API_CLEARED, Styles.SUCCESS))
|
|
255
|
-
|
|
256
|
-
if set_model_option is not None:
|
|
257
|
-
set_model(set_model_option)
|
|
395
|
+
if updates.model_set and set_model_option is not None:
|
|
258
396
|
console.print(
|
|
259
397
|
_styled(Messages.INFO_MODEL_SET.format(value=set_model_option), Styles.SUCCESS)
|
|
260
398
|
)
|
|
261
|
-
|
|
262
|
-
if set_batch_option is not None:
|
|
263
|
-
if set_batch_option < 0:
|
|
264
|
-
raise typer.BadParameter(Messages.ERROR_BATCH_NEGATIVE)
|
|
265
|
-
set_batch_size(set_batch_option)
|
|
399
|
+
if updates.batch_size_set and set_batch_option is not None:
|
|
266
400
|
console.print(
|
|
267
401
|
_styled(Messages.INFO_BATCH_SET.format(value=set_batch_option), Styles.SUCCESS)
|
|
268
402
|
)
|
|
269
|
-
|
|
403
|
+
if updates.provider_set and set_provider_option is not None:
|
|
404
|
+
console.print(
|
|
405
|
+
_styled(Messages.INFO_PROVIDER_SET.format(value=set_provider_option), Styles.SUCCESS)
|
|
406
|
+
)
|
|
407
|
+
if updates.base_url_set and set_base_url_option is not None:
|
|
408
|
+
console.print(
|
|
409
|
+
_styled(Messages.INFO_BASE_URL_SET.format(value=set_base_url_option), Styles.SUCCESS)
|
|
410
|
+
)
|
|
411
|
+
if updates.base_url_cleared and clear_base_url:
|
|
412
|
+
console.print(_styled(Messages.INFO_BASE_URL_CLEARED, Styles.SUCCESS))
|
|
413
|
+
|
|
414
|
+
if clear_index_all:
|
|
415
|
+
removed = clear_all_cache()
|
|
416
|
+
if removed:
|
|
417
|
+
plural = "ies" if removed > 1 else "y"
|
|
418
|
+
console.print(
|
|
419
|
+
_styled(
|
|
420
|
+
Messages.INFO_INDEX_ALL_CLEARED.format(count=removed, plural=plural),
|
|
421
|
+
Styles.SUCCESS,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
console.print(_styled(Messages.INFO_INDEX_ALL_CLEAR_NONE, Styles.INFO))
|
|
426
|
+
|
|
427
|
+
should_edit = not any(
|
|
428
|
+
(
|
|
429
|
+
updates.changed,
|
|
430
|
+
show,
|
|
431
|
+
show_index_all,
|
|
432
|
+
clear_index_all,
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
if should_edit:
|
|
436
|
+
_edit_config_file()
|
|
437
|
+
return
|
|
270
438
|
|
|
271
|
-
if show
|
|
272
|
-
cfg =
|
|
439
|
+
if show:
|
|
440
|
+
cfg = get_config_snapshot()
|
|
273
441
|
console.print(
|
|
274
442
|
_styled(
|
|
275
443
|
Messages.INFO_CONFIG_SUMMARY.format(
|
|
276
444
|
api="yes" if cfg.api_key else "no",
|
|
445
|
+
provider=cfg.provider or DEFAULT_PROVIDER,
|
|
277
446
|
model=cfg.model or DEFAULT_MODEL,
|
|
278
447
|
batch=cfg.batch_size if cfg.batch_size is not None else DEFAULT_BATCH_SIZE,
|
|
448
|
+
base_url=cfg.base_url or "none",
|
|
279
449
|
),
|
|
280
450
|
Styles.INFO,
|
|
281
451
|
)
|
|
282
452
|
)
|
|
283
453
|
|
|
454
|
+
if show_index_all:
|
|
455
|
+
entries = list_cache_entries()
|
|
456
|
+
if not entries:
|
|
457
|
+
console.print(_styled(Messages.INFO_INDEX_ALL_EMPTY, Styles.INFO))
|
|
458
|
+
else:
|
|
459
|
+
console.print(_styled(Messages.INFO_INDEX_ALL_HEADER, Styles.TITLE))
|
|
460
|
+
table = Table(show_header=True, header_style=Styles.TABLE_HEADER)
|
|
461
|
+
table.add_column(Messages.TABLE_INDEX_HEADER_ROOT)
|
|
462
|
+
table.add_column(Messages.TABLE_INDEX_HEADER_MODE)
|
|
463
|
+
table.add_column(Messages.TABLE_INDEX_HEADER_MODEL)
|
|
464
|
+
table.add_column(Messages.TABLE_INDEX_HEADER_HIDDEN, justify="center")
|
|
465
|
+
table.add_column(Messages.TABLE_INDEX_HEADER_RECURSIVE, justify="center")
|
|
466
|
+
table.add_column(Messages.TABLE_INDEX_HEADER_FILES, justify="right")
|
|
467
|
+
table.add_column(Messages.TABLE_INDEX_HEADER_GENERATED)
|
|
468
|
+
for entry in entries:
|
|
469
|
+
table.add_row(
|
|
470
|
+
str(entry["root_path"]),
|
|
471
|
+
str(entry["mode"]),
|
|
472
|
+
str(entry["model"]),
|
|
473
|
+
"yes" if entry["include_hidden"] else "no",
|
|
474
|
+
"yes" if entry["recursive"] else "no",
|
|
475
|
+
str(entry["file_count"]),
|
|
476
|
+
str(entry["generated_at"]),
|
|
477
|
+
)
|
|
478
|
+
console.print(table)
|
|
479
|
+
|
|
284
480
|
|
|
285
481
|
@app.command()
|
|
286
482
|
def doctor() -> None:
|
|
287
483
|
"""Check whether the `vexor` command is available on PATH."""
|
|
288
484
|
console.print(_styled(Messages.INFO_DOCTOR_CHECKING, Styles.INFO))
|
|
289
|
-
command_path =
|
|
485
|
+
command_path = find_command_on_path("vexor")
|
|
290
486
|
if command_path:
|
|
291
487
|
console.print(
|
|
292
488
|
_styled(Messages.INFO_DOCTOR_FOUND.format(path=command_path), Styles.SUCCESS)
|
|
@@ -302,14 +498,14 @@ def update() -> None:
|
|
|
302
498
|
console.print(_styled(Messages.INFO_UPDATE_CHECKING, Styles.INFO))
|
|
303
499
|
console.print(_styled(Messages.INFO_UPDATE_CURRENT.format(current=__version__), Styles.INFO))
|
|
304
500
|
try:
|
|
305
|
-
latest =
|
|
501
|
+
latest = fetch_remote_version(REMOTE_VERSION_URL)
|
|
306
502
|
except RuntimeError as exc:
|
|
307
503
|
console.print(
|
|
308
504
|
_styled(Messages.ERROR_UPDATE_FETCH.format(reason=str(exc)), Styles.ERROR)
|
|
309
505
|
)
|
|
310
506
|
raise typer.Exit(code=1)
|
|
311
507
|
|
|
312
|
-
if
|
|
508
|
+
if version_tuple(latest) > version_tuple(__version__):
|
|
313
509
|
console.print(
|
|
314
510
|
_styled(
|
|
315
511
|
Messages.INFO_UPDATE_AVAILABLE.format(
|
|
@@ -327,7 +523,7 @@ def update() -> None:
|
|
|
327
523
|
)
|
|
328
524
|
|
|
329
525
|
|
|
330
|
-
def _render_results(results: Sequence[
|
|
526
|
+
def _render_results(results: Sequence["SearchResult"], base: Path, backend: str | None) -> None:
|
|
331
527
|
console.print(_styled(Messages.TABLE_TITLE, Styles.TITLE))
|
|
332
528
|
if backend:
|
|
333
529
|
console.print(_styled(f"{Messages.TABLE_BACKEND_PREFIX}{backend}", Styles.INFO))
|
|
@@ -335,132 +531,83 @@ def _render_results(results: Sequence[DisplayResult], base: Path, backend: str |
|
|
|
335
531
|
table.add_column(Messages.TABLE_HEADER_INDEX, justify="right")
|
|
336
532
|
table.add_column(Messages.TABLE_HEADER_SIMILARITY, justify="right")
|
|
337
533
|
table.add_column(Messages.TABLE_HEADER_PATH, overflow="fold")
|
|
534
|
+
table.add_column(Messages.TABLE_HEADER_PREVIEW, overflow="fold")
|
|
338
535
|
for idx, result in enumerate(results, start=1):
|
|
339
536
|
table.add_row(
|
|
340
537
|
str(idx),
|
|
341
538
|
f"{result.score:.3f}",
|
|
342
539
|
format_path(result.path, base),
|
|
540
|
+
_format_preview(result.preview),
|
|
343
541
|
)
|
|
344
542
|
console.print(table)
|
|
345
543
|
|
|
346
544
|
|
|
347
|
-
def
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return VexorSearcher(model_name=model_name, batch_size=batch_size)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def _label_for_path(path: Path) -> str:
|
|
354
|
-
return path.name.replace("_", " ")
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def _load_index(root: Path, model: str, include_hidden: bool):
|
|
358
|
-
from .cache import load_index_vectors # local import
|
|
359
|
-
|
|
360
|
-
return load_index_vectors(root, model, include_hidden)
|
|
361
|
-
|
|
545
|
+
def _styled(text: str, style: str) -> str:
|
|
546
|
+
return f"[{style}]{text}[/{style}]"
|
|
362
547
|
|
|
363
|
-
def _load_index_metadata_safe(root: Path, model: str, include_hidden: bool):
|
|
364
|
-
from .cache import load_index # local import
|
|
365
548
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
549
|
+
def _format_preview(text: str | None, limit: int = 80) -> str:
|
|
550
|
+
if not text:
|
|
551
|
+
return "-"
|
|
552
|
+
snippet = text.strip()
|
|
553
|
+
if len(snippet) <= limit:
|
|
554
|
+
return snippet
|
|
555
|
+
return snippet[: limit - 1].rstrip() + "…"
|
|
370
556
|
|
|
371
557
|
|
|
372
|
-
def
|
|
373
|
-
|
|
558
|
+
def run(argv: list[str] | None = None) -> None:
|
|
559
|
+
"""Entry point wrapper allowing optional argument override."""
|
|
560
|
+
if argv is None:
|
|
561
|
+
app()
|
|
562
|
+
else:
|
|
563
|
+
app(args=list(argv))
|
|
374
564
|
|
|
375
|
-
return store_index(**kwargs)
|
|
376
565
|
|
|
566
|
+
def _format_command(parts: Sequence[str]) -> str:
|
|
567
|
+
return " ".join(shlex.quote(part) for part in parts)
|
|
377
568
|
|
|
378
|
-
def _clear_index_cache(root: Path, include_hidden: bool, model: str | None = None) -> int:
|
|
379
|
-
from .cache import clear_index # local import
|
|
380
569
|
|
|
381
|
-
|
|
570
|
+
def _ensure_config_file() -> Path:
|
|
571
|
+
config_path = config_module.CONFIG_FILE
|
|
572
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
573
|
+
if not config_path.exists():
|
|
574
|
+
config_path.write_text("{}\n", encoding="utf-8")
|
|
575
|
+
return config_path
|
|
382
576
|
|
|
383
577
|
|
|
384
|
-
def
|
|
385
|
-
|
|
578
|
+
def _edit_config_file() -> None:
|
|
579
|
+
command = resolve_editor_command()
|
|
580
|
+
if not command:
|
|
581
|
+
console.print(_styled(Messages.ERROR_CONFIG_EDITOR_NOT_FOUND, Styles.ERROR))
|
|
582
|
+
raise typer.Exit(code=1)
|
|
386
583
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
raise RuntimeError("Version string not found")
|
|
398
|
-
return match.group(1)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def _version_tuple(raw: str) -> tuple[int, int, int, int]:
|
|
402
|
-
raw = raw.strip()
|
|
403
|
-
release_parts: list[int] = []
|
|
404
|
-
suffix_number = 0
|
|
405
|
-
|
|
406
|
-
for piece in raw.split('.'):
|
|
407
|
-
match = re.match(r"^(\d+)", piece)
|
|
408
|
-
if not match:
|
|
409
|
-
break
|
|
410
|
-
release_parts.append(int(match.group(1)))
|
|
411
|
-
remainder = piece[match.end():]
|
|
412
|
-
if remainder:
|
|
413
|
-
suffix_match = re.match(r"[A-Za-z]+(\d+)", remainder)
|
|
414
|
-
if suffix_match:
|
|
415
|
-
suffix_number = int(suffix_match.group(1))
|
|
416
|
-
break
|
|
417
|
-
if len(release_parts) >= 4:
|
|
418
|
-
break
|
|
419
|
-
|
|
420
|
-
while len(release_parts) < 4:
|
|
421
|
-
release_parts.append(0)
|
|
422
|
-
|
|
423
|
-
if suffix_number:
|
|
424
|
-
release_parts[3] = suffix_number
|
|
425
|
-
|
|
426
|
-
return tuple(release_parts[:4])
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def _is_cache_current(
|
|
430
|
-
root: Path,
|
|
431
|
-
include_hidden: bool,
|
|
432
|
-
cached_files: Sequence[dict],
|
|
433
|
-
*,
|
|
434
|
-
current_files: Sequence[Path] | None = None,
|
|
435
|
-
) -> bool:
|
|
436
|
-
if not cached_files:
|
|
437
|
-
return False
|
|
438
|
-
from .cache import compare_snapshot # local import
|
|
439
|
-
|
|
440
|
-
return compare_snapshot(
|
|
441
|
-
root,
|
|
442
|
-
include_hidden,
|
|
443
|
-
cached_files,
|
|
444
|
-
current_files=current_files,
|
|
584
|
+
cmd_list = list(command)
|
|
585
|
+
config_path = _ensure_config_file()
|
|
586
|
+
console.print(
|
|
587
|
+
_styled(
|
|
588
|
+
Messages.INFO_CONFIG_EDITING.format(
|
|
589
|
+
path=config_path,
|
|
590
|
+
editor=_format_command(cmd_list),
|
|
591
|
+
),
|
|
592
|
+
Styles.INFO,
|
|
593
|
+
)
|
|
445
594
|
)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
595
|
+
try:
|
|
596
|
+
subprocess.run(cmd_list + [str(config_path)], check=True)
|
|
597
|
+
except FileNotFoundError as exc:
|
|
598
|
+
console.print(
|
|
599
|
+
_styled(
|
|
600
|
+
Messages.ERROR_CONFIG_EDITOR_LAUNCH.format(reason=str(exc)),
|
|
601
|
+
Styles.ERROR,
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
raise typer.Exit(code=1)
|
|
605
|
+
except subprocess.CalledProcessError as exc:
|
|
606
|
+
code = exc.returncode if exc.returncode is not None else 1
|
|
452
607
|
console.print(
|
|
453
|
-
_styled(
|
|
608
|
+
_styled(
|
|
609
|
+
Messages.ERROR_CONFIG_EDITOR_FAILED.format(code=code),
|
|
610
|
+
Styles.ERROR,
|
|
611
|
+
)
|
|
454
612
|
)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
def _styled(text: str, style: str) -> str:
|
|
458
|
-
return f"[{style}]{text}[/{style}]"
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def run(argv: list[str] | None = None) -> None:
|
|
462
|
-
"""Entry point wrapper allowing optional argument override."""
|
|
463
|
-
if argv is None:
|
|
464
|
-
app()
|
|
465
|
-
else:
|
|
466
|
-
app(args=list(argv))
|
|
613
|
+
raise typer.Exit(code=code)
|