kodit 0.4.3__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.
Potentially problematic release.
This version of kodit might be problematic. Click here for more details.
- kodit/_version.py +2 -2
- kodit/app.py +53 -23
- kodit/application/factories/reporting_factory.py +6 -2
- kodit/application/factories/server_factory.py +311 -0
- kodit/application/services/code_search_application_service.py +144 -0
- kodit/application/services/commit_indexing_application_service.py +543 -0
- kodit/application/services/indexing_worker_service.py +13 -44
- kodit/application/services/queue_service.py +24 -3
- kodit/application/services/reporting.py +0 -2
- kodit/application/services/sync_scheduler.py +15 -31
- kodit/cli.py +2 -753
- kodit/cli_utils.py +2 -9
- kodit/config.py +1 -94
- kodit/database.py +38 -1
- kodit/domain/{entities.py → entities/__init__.py} +50 -195
- kodit/domain/entities/git.py +190 -0
- kodit/domain/factories/__init__.py +1 -0
- kodit/domain/factories/git_repo_factory.py +76 -0
- kodit/domain/protocols.py +263 -64
- kodit/domain/services/bm25_service.py +5 -1
- kodit/domain/services/embedding_service.py +3 -0
- kodit/domain/services/git_repository_service.py +429 -0
- kodit/domain/services/git_service.py +300 -0
- kodit/domain/services/task_status_query_service.py +2 -2
- kodit/domain/value_objects.py +83 -114
- kodit/infrastructure/api/client/__init__.py +0 -2
- kodit/infrastructure/api/v1/__init__.py +0 -4
- kodit/infrastructure/api/v1/dependencies.py +92 -46
- kodit/infrastructure/api/v1/routers/__init__.py +0 -6
- kodit/infrastructure/api/v1/routers/commits.py +271 -0
- kodit/infrastructure/api/v1/routers/queue.py +2 -2
- kodit/infrastructure/api/v1/routers/repositories.py +282 -0
- kodit/infrastructure/api/v1/routers/search.py +31 -14
- kodit/infrastructure/api/v1/schemas/__init__.py +0 -24
- kodit/infrastructure/api/v1/schemas/commit.py +96 -0
- kodit/infrastructure/api/v1/schemas/context.py +2 -0
- kodit/infrastructure/api/v1/schemas/repository.py +128 -0
- kodit/infrastructure/api/v1/schemas/search.py +12 -9
- kodit/infrastructure/api/v1/schemas/snippet.py +58 -0
- kodit/infrastructure/api/v1/schemas/tag.py +31 -0
- kodit/infrastructure/api/v1/schemas/task_status.py +2 -0
- kodit/infrastructure/bm25/local_bm25_repository.py +16 -4
- kodit/infrastructure/bm25/vectorchord_bm25_repository.py +68 -52
- kodit/infrastructure/cloning/git/git_python_adaptor.py +467 -0
- kodit/infrastructure/cloning/git/working_copy.py +1 -1
- kodit/infrastructure/embedding/embedding_factory.py +3 -2
- kodit/infrastructure/embedding/local_vector_search_repository.py +1 -1
- kodit/infrastructure/embedding/vectorchord_vector_search_repository.py +111 -84
- kodit/infrastructure/enrichment/litellm_enrichment_provider.py +19 -26
- kodit/infrastructure/indexing/fusion_service.py +1 -1
- kodit/infrastructure/mappers/git_mapper.py +193 -0
- kodit/infrastructure/mappers/snippet_mapper.py +106 -0
- kodit/infrastructure/mappers/task_mapper.py +5 -44
- kodit/infrastructure/reporting/log_progress.py +8 -5
- kodit/infrastructure/reporting/telemetry_progress.py +21 -0
- kodit/infrastructure/slicing/slicer.py +32 -31
- kodit/infrastructure/sqlalchemy/embedding_repository.py +43 -23
- kodit/infrastructure/sqlalchemy/entities.py +394 -158
- kodit/infrastructure/sqlalchemy/git_branch_repository.py +263 -0
- kodit/infrastructure/sqlalchemy/git_commit_repository.py +337 -0
- kodit/infrastructure/sqlalchemy/git_repository.py +252 -0
- kodit/infrastructure/sqlalchemy/git_tag_repository.py +257 -0
- kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +484 -0
- kodit/infrastructure/sqlalchemy/task_repository.py +29 -23
- kodit/infrastructure/sqlalchemy/task_status_repository.py +24 -12
- kodit/infrastructure/sqlalchemy/unit_of_work.py +10 -14
- kodit/mcp.py +12 -30
- kodit/migrations/env.py +1 -0
- kodit/migrations/versions/04b80f802e0c_foreign_key_review.py +100 -0
- kodit/migrations/versions/7f15f878c3a1_add_new_git_entities.py +690 -0
- kodit/migrations/versions/f9e5ef5e688f_add_git_commits_number.py +43 -0
- kodit/py.typed +0 -0
- kodit/utils/dump_openapi.py +7 -4
- kodit/utils/path_utils.py +29 -0
- {kodit-0.4.3.dist-info → kodit-0.5.0.dist-info}/METADATA +3 -3
- kodit-0.5.0.dist-info/RECORD +137 -0
- kodit/application/factories/code_indexing_factory.py +0 -195
- kodit/application/services/auto_indexing_service.py +0 -99
- kodit/application/services/code_indexing_application_service.py +0 -410
- kodit/domain/services/index_query_service.py +0 -70
- kodit/domain/services/index_service.py +0 -269
- kodit/infrastructure/api/client/index_client.py +0 -57
- kodit/infrastructure/api/v1/routers/indexes.py +0 -164
- kodit/infrastructure/api/v1/schemas/index.py +0 -101
- kodit/infrastructure/bm25/bm25_factory.py +0 -28
- kodit/infrastructure/cloning/__init__.py +0 -1
- kodit/infrastructure/cloning/metadata.py +0 -98
- kodit/infrastructure/mappers/index_mapper.py +0 -345
- kodit/infrastructure/reporting/tdqm_progress.py +0 -38
- kodit/infrastructure/slicing/language_detection_service.py +0 -18
- kodit/infrastructure/sqlalchemy/index_repository.py +0 -646
- kodit-0.4.3.dist-info/RECORD +0 -125
- {kodit-0.4.3.dist-info → kodit-0.5.0.dist-info}/WHEEL +0 -0
- {kodit-0.4.3.dist-info → kodit-0.5.0.dist-info}/entry_points.txt +0 -0
- {kodit-0.4.3.dist-info → kodit-0.5.0.dist-info}/licenses/LICENSE +0 -0
kodit/cli.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# ruff: noqa: ARG001
|
|
1
2
|
"""Command line interface for kodit."""
|
|
2
3
|
|
|
3
4
|
import signal
|
|
@@ -8,26 +9,10 @@ from typing import Any
|
|
|
8
9
|
import click
|
|
9
10
|
import structlog
|
|
10
11
|
import uvicorn
|
|
11
|
-
from pytable_formatter import Cell, Table # type: ignore[import-untyped]
|
|
12
12
|
|
|
13
|
-
from kodit.application.factories.code_indexing_factory import (
|
|
14
|
-
create_cli_code_indexing_application_service,
|
|
15
|
-
)
|
|
16
13
|
from kodit.config import (
|
|
17
14
|
AppContext,
|
|
18
|
-
with_app_context,
|
|
19
|
-
wrap_async,
|
|
20
|
-
)
|
|
21
|
-
from kodit.domain.errors import EmptySourceError
|
|
22
|
-
from kodit.domain.services.index_query_service import IndexQueryService
|
|
23
|
-
from kodit.domain.value_objects import (
|
|
24
|
-
MultiSearchRequest,
|
|
25
|
-
MultiSearchResult,
|
|
26
|
-
SnippetSearchFilters,
|
|
27
15
|
)
|
|
28
|
-
from kodit.infrastructure.api.client import IndexClient, SearchClient
|
|
29
|
-
from kodit.infrastructure.indexing.fusion_service import ReciprocalRankFusionService
|
|
30
|
-
from kodit.infrastructure.sqlalchemy.index_repository import create_index_repository
|
|
31
16
|
from kodit.log import configure_logging, configure_telemetry, log_event
|
|
32
17
|
from kodit.mcp import create_stdio_mcp_server
|
|
33
18
|
|
|
@@ -61,743 +46,6 @@ def cli(
|
|
|
61
46
|
ctx.obj = config
|
|
62
47
|
|
|
63
48
|
|
|
64
|
-
async def _handle_auto_index(
|
|
65
|
-
app_context: AppContext,
|
|
66
|
-
sources: list[str], # noqa: ARG001
|
|
67
|
-
) -> list[str]:
|
|
68
|
-
"""Handle auto-index option and return sources to process."""
|
|
69
|
-
log = structlog.get_logger(__name__)
|
|
70
|
-
log.info("Auto-indexing configuration", config=app_context.auto_indexing)
|
|
71
|
-
if not app_context.auto_indexing or not app_context.auto_indexing.sources:
|
|
72
|
-
click.echo("No auto-index sources configured.")
|
|
73
|
-
return []
|
|
74
|
-
auto_sources = app_context.auto_indexing.sources
|
|
75
|
-
click.echo(f"Auto-indexing {len(auto_sources)} configured sources...")
|
|
76
|
-
return [source.uri for source in auto_sources]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
async def _handle_sync(
|
|
80
|
-
service: Any,
|
|
81
|
-
index_query_service: IndexQueryService,
|
|
82
|
-
sources: list[str],
|
|
83
|
-
) -> None:
|
|
84
|
-
"""Handle sync operation."""
|
|
85
|
-
log = structlog.get_logger(__name__)
|
|
86
|
-
log_event("kodit.cli.index.sync")
|
|
87
|
-
|
|
88
|
-
# Get all existing indexes
|
|
89
|
-
all_indexes = await index_query_service.list_indexes()
|
|
90
|
-
|
|
91
|
-
if not all_indexes:
|
|
92
|
-
click.echo("No existing indexes found to sync.")
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
# Filter indexes if specific sources are provided
|
|
96
|
-
indexes_to_sync = all_indexes
|
|
97
|
-
if sources:
|
|
98
|
-
# Filter indexes that match the provided sources
|
|
99
|
-
source_uris = set(sources)
|
|
100
|
-
indexes_to_sync = [
|
|
101
|
-
index
|
|
102
|
-
for index in all_indexes
|
|
103
|
-
if str(index.source.working_copy.remote_uri) in source_uris
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
if not indexes_to_sync:
|
|
107
|
-
click.echo(
|
|
108
|
-
f"No indexes found for the specified sources: {', '.join(sources)}"
|
|
109
|
-
)
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
click.echo(f"Syncing {len(indexes_to_sync)} indexes...")
|
|
113
|
-
|
|
114
|
-
# Sync each index
|
|
115
|
-
for index in indexes_to_sync:
|
|
116
|
-
click.echo(f"Syncing: {index.source.working_copy.remote_uri}")
|
|
117
|
-
|
|
118
|
-
try:
|
|
119
|
-
await service.run_index(index)
|
|
120
|
-
click.echo(f"✓ Sync completed: {index.source.working_copy.remote_uri}")
|
|
121
|
-
except Exception as e:
|
|
122
|
-
log.exception("Sync failed", index_id=index.id, error=e)
|
|
123
|
-
click.echo(f"✗ Sync failed: {index.source.working_copy.remote_uri} - {e}")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
async def _handle_list_indexes(index_query_service: IndexQueryService) -> None:
|
|
127
|
-
"""Handle listing all indexes."""
|
|
128
|
-
log_event("kodit.cli.index.list")
|
|
129
|
-
# No source specified, list all indexes
|
|
130
|
-
indexes = await index_query_service.list_indexes()
|
|
131
|
-
headers: list[str | Cell] = [
|
|
132
|
-
"ID",
|
|
133
|
-
"Created At",
|
|
134
|
-
"Updated At",
|
|
135
|
-
"Source",
|
|
136
|
-
"Num Snippets",
|
|
137
|
-
]
|
|
138
|
-
data = [
|
|
139
|
-
[
|
|
140
|
-
index.id,
|
|
141
|
-
index.created_at,
|
|
142
|
-
index.updated_at,
|
|
143
|
-
index.source.working_copy.remote_uri,
|
|
144
|
-
len(index.source.working_copy.files),
|
|
145
|
-
]
|
|
146
|
-
for index in indexes
|
|
147
|
-
]
|
|
148
|
-
click.echo(Table(headers=headers, data=data))
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@cli.command()
|
|
152
|
-
@click.argument("sources", nargs=-1)
|
|
153
|
-
@click.option(
|
|
154
|
-
"--auto-index", is_flag=True, help="Index all configured auto-index sources"
|
|
155
|
-
)
|
|
156
|
-
@click.option("--sync", is_flag=True, help="Sync existing indexes with their remotes")
|
|
157
|
-
@with_app_context
|
|
158
|
-
@wrap_async
|
|
159
|
-
async def index(
|
|
160
|
-
app_context: AppContext,
|
|
161
|
-
sources: list[str],
|
|
162
|
-
*, # Force keyword-only arguments
|
|
163
|
-
auto_index: bool,
|
|
164
|
-
sync: bool,
|
|
165
|
-
) -> None:
|
|
166
|
-
"""List indexes, index data sources, or sync existing indexes."""
|
|
167
|
-
if not app_context.is_remote:
|
|
168
|
-
# Local mode - use existing implementation
|
|
169
|
-
await _index_local(app_context, sources, auto_index, sync)
|
|
170
|
-
else:
|
|
171
|
-
# Remote mode - use API clients
|
|
172
|
-
await _index_remote(app_context, sources, auto_index, sync)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
async def _index_local(
|
|
176
|
-
app_context: AppContext,
|
|
177
|
-
sources: list[str],
|
|
178
|
-
auto_index: bool, # noqa: FBT001
|
|
179
|
-
sync: bool, # noqa: FBT001
|
|
180
|
-
) -> None:
|
|
181
|
-
"""Handle index operations in local mode."""
|
|
182
|
-
log = structlog.get_logger(__name__)
|
|
183
|
-
|
|
184
|
-
# Get database session
|
|
185
|
-
db = await app_context.get_db()
|
|
186
|
-
service = create_cli_code_indexing_application_service(
|
|
187
|
-
app_context=app_context,
|
|
188
|
-
session_factory=db.session_factory,
|
|
189
|
-
)
|
|
190
|
-
index_query_service = IndexQueryService(
|
|
191
|
-
index_repository=create_index_repository(session_factory=db.session_factory),
|
|
192
|
-
fusion_service=ReciprocalRankFusionService(),
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
if auto_index:
|
|
196
|
-
sources = await _handle_auto_index(app_context, sources)
|
|
197
|
-
if not sources:
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
if sync:
|
|
201
|
-
await _handle_sync(service, index_query_service, sources)
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
if not sources:
|
|
205
|
-
await _handle_list_indexes(index_query_service)
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
# Handle source indexing
|
|
209
|
-
for source in sources:
|
|
210
|
-
if Path(source).is_file():
|
|
211
|
-
msg = "File indexing is not implemented yet"
|
|
212
|
-
raise click.UsageError(msg)
|
|
213
|
-
|
|
214
|
-
# Index source with progress
|
|
215
|
-
log_event("kodit.cli.index.create")
|
|
216
|
-
|
|
217
|
-
# Create a lazy progress callback that only shows progress when needed
|
|
218
|
-
index = await service.create_index_from_uri(source)
|
|
219
|
-
|
|
220
|
-
# Create a new progress callback for the indexing operations
|
|
221
|
-
try:
|
|
222
|
-
await service.run_index(index)
|
|
223
|
-
except EmptySourceError as e:
|
|
224
|
-
log.exception("Empty source error", error=e)
|
|
225
|
-
msg = f"""{e}. This could mean:
|
|
226
|
-
• The repository contains no supported file types
|
|
227
|
-
• All files are excluded by ignore patterns
|
|
228
|
-
• The files contain no extractable code snippets
|
|
229
|
-
Please check the repository contents and try again.
|
|
230
|
-
"""
|
|
231
|
-
click.echo(msg)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
async def _index_remote(
|
|
235
|
-
app_context: AppContext,
|
|
236
|
-
sources: list[str],
|
|
237
|
-
auto_index: bool, # noqa: FBT001
|
|
238
|
-
sync: bool, # noqa: FBT001
|
|
239
|
-
) -> None:
|
|
240
|
-
"""Handle index operations in remote mode."""
|
|
241
|
-
if sync:
|
|
242
|
-
# Sync operation not available in remote mode
|
|
243
|
-
click.echo("⚠️ Warning: Index sync is not implemented in remote mode")
|
|
244
|
-
click.echo("Please use the server's auto-sync functionality or sync locally")
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
if auto_index:
|
|
248
|
-
click.echo("⚠️ Warning: Auto-index is not implemented in remote mode")
|
|
249
|
-
click.echo("Please configure sources to be auto-indexed on the server")
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
# Create API client
|
|
253
|
-
index_client = IndexClient(
|
|
254
|
-
base_url=app_context.remote.server_url or "",
|
|
255
|
-
api_key=app_context.remote.api_key,
|
|
256
|
-
timeout=app_context.remote.timeout,
|
|
257
|
-
max_retries=app_context.remote.max_retries,
|
|
258
|
-
verify_ssl=app_context.remote.verify_ssl,
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
try:
|
|
262
|
-
if not sources:
|
|
263
|
-
# List indexes
|
|
264
|
-
indexes = await index_client.list_indexes()
|
|
265
|
-
_display_indexes_remote(indexes)
|
|
266
|
-
return
|
|
267
|
-
|
|
268
|
-
# Create new indexes
|
|
269
|
-
for source in sources:
|
|
270
|
-
click.echo(f"Creating index for: {source}")
|
|
271
|
-
index = await index_client.create_index(source)
|
|
272
|
-
click.echo(f"✓ Index created with ID: {index.id}")
|
|
273
|
-
|
|
274
|
-
finally:
|
|
275
|
-
await index_client.close()
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _display_indexes_remote(indexes: list[Any]) -> None:
|
|
279
|
-
"""Display indexes for remote mode."""
|
|
280
|
-
headers = [
|
|
281
|
-
"ID",
|
|
282
|
-
"Created At",
|
|
283
|
-
"Updated At",
|
|
284
|
-
"Source",
|
|
285
|
-
]
|
|
286
|
-
data = [
|
|
287
|
-
[
|
|
288
|
-
index.id,
|
|
289
|
-
index.attributes.created_at,
|
|
290
|
-
index.attributes.updated_at,
|
|
291
|
-
index.attributes.uri,
|
|
292
|
-
]
|
|
293
|
-
for index in indexes
|
|
294
|
-
]
|
|
295
|
-
|
|
296
|
-
click.echo(Table(headers=headers, data=data))
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
async def _search_local( # noqa: PLR0913
|
|
300
|
-
app_context: AppContext,
|
|
301
|
-
keywords: list[str] | None = None,
|
|
302
|
-
code_query: str | None = None,
|
|
303
|
-
text_query: str | None = None,
|
|
304
|
-
top_k: int = 10,
|
|
305
|
-
language: str | None = None,
|
|
306
|
-
author: str | None = None,
|
|
307
|
-
created_after: str | None = None,
|
|
308
|
-
created_before: str | None = None,
|
|
309
|
-
source_repo: str | None = None,
|
|
310
|
-
output_format: str = "text",
|
|
311
|
-
event_name: str = "kodit.cli.search",
|
|
312
|
-
) -> None:
|
|
313
|
-
"""Handle search operations in local mode."""
|
|
314
|
-
log_event(event_name)
|
|
315
|
-
|
|
316
|
-
# Get database session
|
|
317
|
-
db = await app_context.get_db()
|
|
318
|
-
service = create_cli_code_indexing_application_service(
|
|
319
|
-
app_context=app_context,
|
|
320
|
-
session_factory=db.session_factory,
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
filters = _parse_filters(
|
|
324
|
-
language, author, created_after, created_before, source_repo
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
snippets = await service.search(
|
|
328
|
-
MultiSearchRequest(
|
|
329
|
-
keywords=keywords,
|
|
330
|
-
code_query=code_query,
|
|
331
|
-
text_query=text_query,
|
|
332
|
-
top_k=top_k,
|
|
333
|
-
filters=filters,
|
|
334
|
-
)
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
if len(snippets) == 0:
|
|
338
|
-
click.echo("No snippets found")
|
|
339
|
-
return
|
|
340
|
-
|
|
341
|
-
if output_format == "text":
|
|
342
|
-
click.echo(MultiSearchResult.to_string(snippets))
|
|
343
|
-
elif output_format == "json":
|
|
344
|
-
click.echo(MultiSearchResult.to_jsonlines(snippets))
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
async def _search_remote( # noqa: PLR0913
|
|
348
|
-
app_context: AppContext,
|
|
349
|
-
keywords: list[str] | None = None,
|
|
350
|
-
code_query: str | None = None,
|
|
351
|
-
text_query: str | None = None,
|
|
352
|
-
top_k: int = 10,
|
|
353
|
-
language: str | None = None,
|
|
354
|
-
author: str | None = None,
|
|
355
|
-
created_after: str | None = None,
|
|
356
|
-
created_before: str | None = None,
|
|
357
|
-
source_repo: str | None = None,
|
|
358
|
-
output_format: str = "text",
|
|
359
|
-
) -> None:
|
|
360
|
-
"""Handle search operations in remote mode."""
|
|
361
|
-
from datetime import datetime
|
|
362
|
-
|
|
363
|
-
# Parse date filters for API
|
|
364
|
-
parsed_start_date = None
|
|
365
|
-
if created_after:
|
|
366
|
-
try:
|
|
367
|
-
parsed_start_date = datetime.fromisoformat(created_after)
|
|
368
|
-
except ValueError as err:
|
|
369
|
-
raise ValueError(
|
|
370
|
-
f"Invalid date format for --created-after: {created_after}. "
|
|
371
|
-
"Expected ISO 8601 format (YYYY-MM-DD)"
|
|
372
|
-
) from err
|
|
373
|
-
|
|
374
|
-
parsed_end_date = None
|
|
375
|
-
if created_before:
|
|
376
|
-
try:
|
|
377
|
-
parsed_end_date = datetime.fromisoformat(created_before)
|
|
378
|
-
except ValueError as err:
|
|
379
|
-
raise ValueError(
|
|
380
|
-
f"Invalid date format for --created-before: {created_before}. "
|
|
381
|
-
"Expected ISO 8601 format (YYYY-MM-DD)"
|
|
382
|
-
) from err
|
|
383
|
-
|
|
384
|
-
# Create API client
|
|
385
|
-
search_client = SearchClient(
|
|
386
|
-
base_url=app_context.remote.server_url or "",
|
|
387
|
-
api_key=app_context.remote.api_key,
|
|
388
|
-
timeout=app_context.remote.timeout,
|
|
389
|
-
max_retries=app_context.remote.max_retries,
|
|
390
|
-
verify_ssl=app_context.remote.verify_ssl,
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
try:
|
|
394
|
-
# Perform search
|
|
395
|
-
snippets = await search_client.search(
|
|
396
|
-
keywords=keywords,
|
|
397
|
-
code_query=code_query,
|
|
398
|
-
text_query=text_query,
|
|
399
|
-
limit=top_k,
|
|
400
|
-
languages=[language] if language else None,
|
|
401
|
-
authors=[author] if author else None,
|
|
402
|
-
start_date=parsed_start_date,
|
|
403
|
-
end_date=parsed_end_date,
|
|
404
|
-
sources=[source_repo] if source_repo else None,
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
if len(snippets) == 0:
|
|
408
|
-
click.echo("No snippets found")
|
|
409
|
-
return
|
|
410
|
-
|
|
411
|
-
if output_format == "text":
|
|
412
|
-
_display_snippets_remote_text(snippets)
|
|
413
|
-
elif output_format == "json":
|
|
414
|
-
_display_snippets_remote_json(snippets)
|
|
415
|
-
|
|
416
|
-
finally:
|
|
417
|
-
await search_client.close()
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def _display_snippets_remote_text(snippets: list[Any]) -> None:
|
|
421
|
-
"""Display snippets in text format for remote mode."""
|
|
422
|
-
for i, snippet in enumerate(snippets):
|
|
423
|
-
click.echo(f"\n--- Snippet {i + 1} ---")
|
|
424
|
-
click.echo(f"ID: {snippet.id}")
|
|
425
|
-
click.echo(f"Language: {snippet.attributes.language}")
|
|
426
|
-
click.echo(f"Source: {snippet.attributes.source_uri}")
|
|
427
|
-
click.echo(f"Path: {snippet.attributes.relative_path}")
|
|
428
|
-
click.echo(f"Authors: {', '.join(snippet.attributes.authors)}")
|
|
429
|
-
click.echo(f"Summary: {snippet.attributes.summary}")
|
|
430
|
-
click.echo("\nContent:")
|
|
431
|
-
click.echo(snippet.attributes.content)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def _display_snippets_remote_json(snippets: list[Any]) -> None:
|
|
435
|
-
"""Display snippets in JSON format for remote mode."""
|
|
436
|
-
import json
|
|
437
|
-
|
|
438
|
-
for snippet in snippets:
|
|
439
|
-
snippet_data = {
|
|
440
|
-
"id": snippet.id,
|
|
441
|
-
"language": snippet.attributes.language,
|
|
442
|
-
"source_uri": snippet.attributes.source_uri,
|
|
443
|
-
"relative_path": snippet.attributes.relative_path,
|
|
444
|
-
"authors": snippet.attributes.authors,
|
|
445
|
-
"summary": snippet.attributes.summary,
|
|
446
|
-
"content": snippet.attributes.content,
|
|
447
|
-
"created_at": snippet.attributes.created_at.isoformat(),
|
|
448
|
-
"updated_at": snippet.attributes.updated_at.isoformat(),
|
|
449
|
-
}
|
|
450
|
-
click.echo(json.dumps(snippet_data))
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
@cli.group()
|
|
454
|
-
def search() -> None:
|
|
455
|
-
"""Search for snippets in the database."""
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
# Utility for robust filter parsing
|
|
459
|
-
def _parse_filters(
|
|
460
|
-
language: str | None,
|
|
461
|
-
author: str | None,
|
|
462
|
-
created_after: str | None,
|
|
463
|
-
created_before: str | None,
|
|
464
|
-
source_repo: str | None,
|
|
465
|
-
) -> SnippetSearchFilters | None:
|
|
466
|
-
from datetime import datetime
|
|
467
|
-
|
|
468
|
-
# Normalize language to lowercase if provided
|
|
469
|
-
norm_language = language.lower() if language else None
|
|
470
|
-
# Try to parse dates, raise error if invalid
|
|
471
|
-
parsed_created_after = None
|
|
472
|
-
if created_after:
|
|
473
|
-
try:
|
|
474
|
-
parsed_created_after = datetime.fromisoformat(created_after)
|
|
475
|
-
except ValueError as err:
|
|
476
|
-
raise ValueError(
|
|
477
|
-
f"Invalid date format for --created-after: {created_after}. "
|
|
478
|
-
"Expected ISO 8601 format (YYYY-MM-DD)"
|
|
479
|
-
) from err
|
|
480
|
-
parsed_created_before = None
|
|
481
|
-
if created_before:
|
|
482
|
-
try:
|
|
483
|
-
parsed_created_before = datetime.fromisoformat(created_before)
|
|
484
|
-
except ValueError as err:
|
|
485
|
-
raise ValueError(
|
|
486
|
-
f"Invalid date format for --created-before: {created_before}. "
|
|
487
|
-
"Expected ISO 8601 format (YYYY-MM-DD)"
|
|
488
|
-
) from err
|
|
489
|
-
# Return None if no filters provided, otherwise return SnippetSearchFilters
|
|
490
|
-
# Check if any original parameters were provided (not just the parsed values)
|
|
491
|
-
if any(
|
|
492
|
-
[
|
|
493
|
-
language,
|
|
494
|
-
author,
|
|
495
|
-
created_after,
|
|
496
|
-
created_before,
|
|
497
|
-
source_repo,
|
|
498
|
-
]
|
|
499
|
-
):
|
|
500
|
-
return SnippetSearchFilters(
|
|
501
|
-
language=norm_language,
|
|
502
|
-
author=author,
|
|
503
|
-
created_after=parsed_created_after,
|
|
504
|
-
created_before=parsed_created_before,
|
|
505
|
-
source_repo=source_repo,
|
|
506
|
-
)
|
|
507
|
-
return None
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
@search.command()
|
|
511
|
-
@click.argument("query")
|
|
512
|
-
@click.option("--top-k", default=10, help="Number of snippets to retrieve")
|
|
513
|
-
@click.option(
|
|
514
|
-
"--language", help="Filter by programming language (e.g., python, go, javascript)"
|
|
515
|
-
)
|
|
516
|
-
@click.option("--author", help="Filter by author name")
|
|
517
|
-
@click.option(
|
|
518
|
-
"--created-after", help="Filter snippets created after this date (YYYY-MM-DD)"
|
|
519
|
-
)
|
|
520
|
-
@click.option(
|
|
521
|
-
"--created-before", help="Filter snippets created before this date (YYYY-MM-DD)"
|
|
522
|
-
)
|
|
523
|
-
@click.option(
|
|
524
|
-
"--source-repo", help="Filter by source repository (e.g., github.com/example/repo)"
|
|
525
|
-
)
|
|
526
|
-
@click.option("--output-format", default="text", help="Format to display snippets in")
|
|
527
|
-
@with_app_context
|
|
528
|
-
@wrap_async
|
|
529
|
-
async def code( # noqa: PLR0913
|
|
530
|
-
app_context: AppContext,
|
|
531
|
-
query: str,
|
|
532
|
-
top_k: int,
|
|
533
|
-
language: str | None,
|
|
534
|
-
author: str | None,
|
|
535
|
-
created_after: str | None,
|
|
536
|
-
created_before: str | None,
|
|
537
|
-
source_repo: str | None,
|
|
538
|
-
output_format: str,
|
|
539
|
-
) -> None:
|
|
540
|
-
"""Search for snippets using semantic code search.
|
|
541
|
-
|
|
542
|
-
This works best if your query is code.
|
|
543
|
-
"""
|
|
544
|
-
if not app_context.is_remote:
|
|
545
|
-
await _search_local(
|
|
546
|
-
app_context,
|
|
547
|
-
code_query=query,
|
|
548
|
-
top_k=top_k,
|
|
549
|
-
language=language,
|
|
550
|
-
author=author,
|
|
551
|
-
created_after=created_after,
|
|
552
|
-
created_before=created_before,
|
|
553
|
-
source_repo=source_repo,
|
|
554
|
-
output_format=output_format,
|
|
555
|
-
event_name="kodit.cli.search.code",
|
|
556
|
-
)
|
|
557
|
-
else:
|
|
558
|
-
await _search_remote(
|
|
559
|
-
app_context,
|
|
560
|
-
code_query=query,
|
|
561
|
-
top_k=top_k,
|
|
562
|
-
language=language,
|
|
563
|
-
author=author,
|
|
564
|
-
created_after=created_after,
|
|
565
|
-
created_before=created_before,
|
|
566
|
-
source_repo=source_repo,
|
|
567
|
-
output_format=output_format,
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
@search.command()
|
|
572
|
-
@click.argument("keywords", nargs=-1)
|
|
573
|
-
@click.option("--top-k", default=10, help="Number of snippets to retrieve")
|
|
574
|
-
@click.option(
|
|
575
|
-
"--language", help="Filter by programming language (e.g., python, go, javascript)"
|
|
576
|
-
)
|
|
577
|
-
@click.option("--author", help="Filter by author name")
|
|
578
|
-
@click.option(
|
|
579
|
-
"--created-after", help="Filter snippets created after this date (YYYY-MM-DD)"
|
|
580
|
-
)
|
|
581
|
-
@click.option(
|
|
582
|
-
"--created-before", help="Filter snippets created before this date (YYYY-MM-DD)"
|
|
583
|
-
)
|
|
584
|
-
@click.option(
|
|
585
|
-
"--source-repo", help="Filter by source repository (e.g., github.com/example/repo)"
|
|
586
|
-
)
|
|
587
|
-
@click.option("--output-format", default="text", help="Format to display snippets in")
|
|
588
|
-
@with_app_context
|
|
589
|
-
@wrap_async
|
|
590
|
-
async def keyword( # noqa: PLR0913
|
|
591
|
-
app_context: AppContext,
|
|
592
|
-
keywords: list[str],
|
|
593
|
-
top_k: int,
|
|
594
|
-
language: str | None,
|
|
595
|
-
author: str | None,
|
|
596
|
-
created_after: str | None,
|
|
597
|
-
created_before: str | None,
|
|
598
|
-
source_repo: str | None,
|
|
599
|
-
output_format: str,
|
|
600
|
-
) -> None:
|
|
601
|
-
"""Search for snippets using keyword search."""
|
|
602
|
-
# Override remote configuration if provided via CLI
|
|
603
|
-
if not app_context.is_remote:
|
|
604
|
-
await _search_local(
|
|
605
|
-
app_context,
|
|
606
|
-
keywords=keywords,
|
|
607
|
-
top_k=top_k,
|
|
608
|
-
language=language,
|
|
609
|
-
author=author,
|
|
610
|
-
created_after=created_after,
|
|
611
|
-
created_before=created_before,
|
|
612
|
-
source_repo=source_repo,
|
|
613
|
-
output_format=output_format,
|
|
614
|
-
event_name="kodit.cli.search.keyword",
|
|
615
|
-
)
|
|
616
|
-
else:
|
|
617
|
-
await _search_remote(
|
|
618
|
-
app_context,
|
|
619
|
-
keywords=keywords,
|
|
620
|
-
top_k=top_k,
|
|
621
|
-
language=language,
|
|
622
|
-
author=author,
|
|
623
|
-
created_after=created_after,
|
|
624
|
-
created_before=created_before,
|
|
625
|
-
source_repo=source_repo,
|
|
626
|
-
output_format=output_format,
|
|
627
|
-
)
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
@search.command()
|
|
631
|
-
@click.argument("query")
|
|
632
|
-
@click.option("--top-k", default=10, help="Number of snippets to retrieve")
|
|
633
|
-
@click.option(
|
|
634
|
-
"--language", help="Filter by programming language (e.g., python, go, javascript)"
|
|
635
|
-
)
|
|
636
|
-
@click.option("--author", help="Filter by author name")
|
|
637
|
-
@click.option(
|
|
638
|
-
"--created-after", help="Filter snippets created after this date (YYYY-MM-DD)"
|
|
639
|
-
)
|
|
640
|
-
@click.option(
|
|
641
|
-
"--created-before", help="Filter snippets created before this date (YYYY-MM-DD)"
|
|
642
|
-
)
|
|
643
|
-
@click.option(
|
|
644
|
-
"--source-repo", help="Filter by source repository (e.g., github.com/example/repo)"
|
|
645
|
-
)
|
|
646
|
-
@click.option("--output-format", default="text", help="Format to display snippets in")
|
|
647
|
-
@with_app_context
|
|
648
|
-
@wrap_async
|
|
649
|
-
async def text( # noqa: PLR0913
|
|
650
|
-
app_context: AppContext,
|
|
651
|
-
query: str,
|
|
652
|
-
top_k: int,
|
|
653
|
-
language: str | None,
|
|
654
|
-
author: str | None,
|
|
655
|
-
created_after: str | None,
|
|
656
|
-
created_before: str | None,
|
|
657
|
-
source_repo: str | None,
|
|
658
|
-
output_format: str,
|
|
659
|
-
) -> None:
|
|
660
|
-
"""Search for snippets using semantic text search.
|
|
661
|
-
|
|
662
|
-
This works best if your query is text.
|
|
663
|
-
"""
|
|
664
|
-
if not app_context.is_remote:
|
|
665
|
-
await _search_local(
|
|
666
|
-
app_context,
|
|
667
|
-
text_query=query,
|
|
668
|
-
top_k=top_k,
|
|
669
|
-
language=language,
|
|
670
|
-
author=author,
|
|
671
|
-
created_after=created_after,
|
|
672
|
-
created_before=created_before,
|
|
673
|
-
source_repo=source_repo,
|
|
674
|
-
output_format=output_format,
|
|
675
|
-
event_name="kodit.cli.search.text",
|
|
676
|
-
)
|
|
677
|
-
else:
|
|
678
|
-
await _search_remote(
|
|
679
|
-
app_context,
|
|
680
|
-
text_query=query,
|
|
681
|
-
top_k=top_k,
|
|
682
|
-
language=language,
|
|
683
|
-
author=author,
|
|
684
|
-
created_after=created_after,
|
|
685
|
-
created_before=created_before,
|
|
686
|
-
source_repo=source_repo,
|
|
687
|
-
output_format=output_format,
|
|
688
|
-
)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
@search.command()
|
|
692
|
-
@click.option("--top-k", default=10, help="Number of snippets to retrieve")
|
|
693
|
-
@click.option("--keywords", required=True, help="Comma separated list of keywords")
|
|
694
|
-
@click.option("--code", required=True, help="Semantic code search query")
|
|
695
|
-
@click.option("--text", required=True, help="Semantic text search query")
|
|
696
|
-
@click.option(
|
|
697
|
-
"--language", help="Filter by programming language (e.g., python, go, javascript)"
|
|
698
|
-
)
|
|
699
|
-
@click.option("--author", help="Filter by author name")
|
|
700
|
-
@click.option(
|
|
701
|
-
"--created-after", help="Filter snippets created after this date (YYYY-MM-DD)"
|
|
702
|
-
)
|
|
703
|
-
@click.option(
|
|
704
|
-
"--created-before", help="Filter snippets created before this date (YYYY-MM-DD)"
|
|
705
|
-
)
|
|
706
|
-
@click.option(
|
|
707
|
-
"--source-repo", help="Filter by source repository (e.g., github.com/example/repo)"
|
|
708
|
-
)
|
|
709
|
-
@click.option("--output-format", default="text", help="Format to display snippets in")
|
|
710
|
-
@with_app_context
|
|
711
|
-
@wrap_async
|
|
712
|
-
async def hybrid( # noqa: PLR0913
|
|
713
|
-
app_context: AppContext,
|
|
714
|
-
top_k: int,
|
|
715
|
-
keywords: str,
|
|
716
|
-
code: str,
|
|
717
|
-
text: str,
|
|
718
|
-
language: str | None,
|
|
719
|
-
author: str | None,
|
|
720
|
-
created_after: str | None,
|
|
721
|
-
created_before: str | None,
|
|
722
|
-
source_repo: str | None,
|
|
723
|
-
output_format: str,
|
|
724
|
-
) -> None:
|
|
725
|
-
"""Search for snippets using hybrid search."""
|
|
726
|
-
# Parse keywords into a list of strings
|
|
727
|
-
keywords_list = [k.strip().lower() for k in keywords.split(",")]
|
|
728
|
-
|
|
729
|
-
if not app_context.is_remote:
|
|
730
|
-
await _search_local(
|
|
731
|
-
app_context,
|
|
732
|
-
keywords=keywords_list,
|
|
733
|
-
code_query=code,
|
|
734
|
-
text_query=text,
|
|
735
|
-
top_k=top_k,
|
|
736
|
-
language=language,
|
|
737
|
-
author=author,
|
|
738
|
-
created_after=created_after,
|
|
739
|
-
created_before=created_before,
|
|
740
|
-
source_repo=source_repo,
|
|
741
|
-
output_format=output_format,
|
|
742
|
-
event_name="kodit.cli.search.hybrid",
|
|
743
|
-
)
|
|
744
|
-
else:
|
|
745
|
-
await _search_remote(
|
|
746
|
-
app_context,
|
|
747
|
-
keywords=keywords_list,
|
|
748
|
-
code_query=code,
|
|
749
|
-
text_query=text,
|
|
750
|
-
top_k=top_k,
|
|
751
|
-
language=language,
|
|
752
|
-
author=author,
|
|
753
|
-
created_after=created_after,
|
|
754
|
-
created_before=created_before,
|
|
755
|
-
source_repo=source_repo,
|
|
756
|
-
output_format=output_format,
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
@cli.group()
|
|
761
|
-
def show() -> None:
|
|
762
|
-
"""Show information about elements in the database."""
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
@show.command()
|
|
766
|
-
@click.option("--by-path", help="File or directory path to search for snippets")
|
|
767
|
-
@click.option("--by-source", help="Source URI to filter snippets by")
|
|
768
|
-
@click.option("--output-format", default="text", help="Format to display snippets in")
|
|
769
|
-
@with_app_context
|
|
770
|
-
@wrap_async
|
|
771
|
-
async def snippets(
|
|
772
|
-
app_context: AppContext,
|
|
773
|
-
by_path: str | None,
|
|
774
|
-
by_source: str | None,
|
|
775
|
-
output_format: str,
|
|
776
|
-
) -> None:
|
|
777
|
-
"""Show snippets with optional filtering by path or source."""
|
|
778
|
-
if not app_context.is_remote:
|
|
779
|
-
# Local mode
|
|
780
|
-
log_event("kodit.cli.show.snippets")
|
|
781
|
-
db = await app_context.get_db()
|
|
782
|
-
service = create_cli_code_indexing_application_service(
|
|
783
|
-
app_context=app_context,
|
|
784
|
-
session_factory=db.session_factory,
|
|
785
|
-
)
|
|
786
|
-
snippets = await service.list_snippets(file_path=by_path, source_uri=by_source)
|
|
787
|
-
if output_format == "text":
|
|
788
|
-
click.echo(MultiSearchResult.to_string(snippets))
|
|
789
|
-
elif output_format == "json":
|
|
790
|
-
click.echo(MultiSearchResult.to_jsonlines(snippets))
|
|
791
|
-
else:
|
|
792
|
-
# Remote mode - not supported
|
|
793
|
-
click.echo("⚠️ Warning: 'show snippets' is not implemented in remote mode")
|
|
794
|
-
click.echo(
|
|
795
|
-
"This functionality is only available when connected directly "
|
|
796
|
-
"to the database"
|
|
797
|
-
)
|
|
798
|
-
click.echo("Use 'kodit search' commands instead for remote snippet retrieval")
|
|
799
|
-
|
|
800
|
-
|
|
801
49
|
@cli.command()
|
|
802
50
|
@click.option("--host", default="127.0.0.1", help="Host to bind the server to")
|
|
803
51
|
@click.option("--port", default=8080, help="Port to bind the server to")
|
|
@@ -813,6 +61,7 @@ def serve(
|
|
|
813
61
|
# Disable uvicorn's websockets deprecation warnings
|
|
814
62
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets")
|
|
815
63
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module="uvicorn")
|
|
64
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="httpx")
|
|
816
65
|
|
|
817
66
|
# Configure uvicorn with graceful shutdown
|
|
818
67
|
config = uvicorn.Config(
|