codemate-cli 1.0.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.
- codemate/__init__.py +17 -0
- codemate/__main__.py +10 -0
- codemate/cli.py +815 -0
- codemate/client.py +311 -0
- codemate/commands/__init__.py +6 -0
- codemate/commands/chat.py +0 -0
- codemate/commands/config.py +103 -0
- codemate/commands/help.py +298 -0
- codemate/commands/kb_commands.py +749 -0
- codemate/config.py +233 -0
- codemate/ui/__init__.py +10 -0
- codemate/ui/markdown.py +212 -0
- codemate/ui/renderer.py +159 -0
- codemate/ui/streaming.py +436 -0
- codemate/utils/__init__.py +21 -0
- codemate/utils/auth.py +164 -0
- codemate/utils/error_handler.py +277 -0
- codemate/utils/errors.py +156 -0
- codemate/utils/kb_parser.py +111 -0
- codemate_cli-1.0.0.dist-info/METADATA +452 -0
- codemate_cli-1.0.0.dist-info/RECORD +25 -0
- codemate_cli-1.0.0.dist-info/WHEEL +5 -0
- codemate_cli-1.0.0.dist-info/entry_points.txt +3 -0
- codemate_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- codemate_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# codemate/commands/kb_commands.py
|
|
2
|
+
"""Knowledge Base Management Commands"""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import platform
|
|
9
|
+
import socketio
|
|
10
|
+
import httpx
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, List, Tuple, Set
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Prompt, Confirm
|
|
16
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich import box
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KBCommands:
|
|
24
|
+
"""Handler for Knowledge Base operations"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config, client):
|
|
27
|
+
self.config = config
|
|
28
|
+
self.client = client
|
|
29
|
+
self.socket_url = "http://localhost:45224"
|
|
30
|
+
self.api_url = "http://localhost:45223"
|
|
31
|
+
|
|
32
|
+
def _normalize_path(self, path_str: str) -> Path:
|
|
33
|
+
"""Normalize path for cross-platform compatibility"""
|
|
34
|
+
path_str = os.path.expanduser(path_str)
|
|
35
|
+
path = Path(path_str).resolve()
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
def _validate_path(self, path: Path) -> Tuple[bool, Optional[str]]:
|
|
39
|
+
"""
|
|
40
|
+
Validate if path exists and is accessible
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Tuple of (is_valid, error_message)
|
|
44
|
+
"""
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return False, f"Path does not exist: {path}"
|
|
47
|
+
|
|
48
|
+
if not path.is_dir():
|
|
49
|
+
return False, f"Path is not a directory: {path}"
|
|
50
|
+
|
|
51
|
+
if not os.access(path, os.R_OK):
|
|
52
|
+
return False, f"No read permission for: {path}"
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if not any(path.iterdir()):
|
|
56
|
+
return False, f"Directory is empty: {path}"
|
|
57
|
+
except PermissionError:
|
|
58
|
+
return False, f"Cannot access directory contents: {path}"
|
|
59
|
+
|
|
60
|
+
return True, None
|
|
61
|
+
|
|
62
|
+
def _parse_gitignore_patterns(self, gitignore_path: Path) -> Set[str]:
|
|
63
|
+
"""Parse .gitignore file and return set of patterns"""
|
|
64
|
+
patterns = set()
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
|
68
|
+
for line in f:
|
|
69
|
+
line = line.strip()
|
|
70
|
+
if line and not line.startswith('#'):
|
|
71
|
+
patterns.add(line)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
console.print(f"[yellow]Warning: Could not read .gitignore: {e}[/yellow]")
|
|
74
|
+
|
|
75
|
+
return patterns
|
|
76
|
+
|
|
77
|
+
def _should_exclude(self, path: Path, exclude_patterns: Set[str], base_path: Path) -> bool:
|
|
78
|
+
"""Check if path matches any exclude pattern"""
|
|
79
|
+
relative_path = path.relative_to(base_path)
|
|
80
|
+
path_str = str(relative_path).replace('\\', '/')
|
|
81
|
+
|
|
82
|
+
for pattern in exclude_patterns:
|
|
83
|
+
if pattern.endswith('/'):
|
|
84
|
+
if path.is_dir() and (path_str + '/').startswith(pattern):
|
|
85
|
+
return True
|
|
86
|
+
if path_str.startswith(pattern.rstrip('/')):
|
|
87
|
+
return True
|
|
88
|
+
elif '*' in pattern:
|
|
89
|
+
import fnmatch
|
|
90
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
91
|
+
return True
|
|
92
|
+
if fnmatch.fnmatch(path.name, pattern):
|
|
93
|
+
return True
|
|
94
|
+
else:
|
|
95
|
+
if path_str == pattern or path.name == pattern:
|
|
96
|
+
return True
|
|
97
|
+
if pattern in path_str.split('/'):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def _collect_files(
|
|
103
|
+
self,
|
|
104
|
+
base_path: Path,
|
|
105
|
+
progress_callback=None
|
|
106
|
+
) -> List[str]:
|
|
107
|
+
"""
|
|
108
|
+
Recursively collect all files from directory, excluding patterns
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of absolute file paths as strings
|
|
112
|
+
"""
|
|
113
|
+
files = []
|
|
114
|
+
total_scanned = 0
|
|
115
|
+
|
|
116
|
+
default_excludes = {
|
|
117
|
+
'.git/', '__pycache__/', 'node_modules/', '.venv/', 'venv/',
|
|
118
|
+
'.env', '.pyc', '.DS_Store', 'Thumbs.db', '.idea/', '.vscode/'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for root, dirs, filenames in os.walk(base_path):
|
|
122
|
+
root_path = Path(root)
|
|
123
|
+
|
|
124
|
+
dirs[:] = [
|
|
125
|
+
d for d in dirs
|
|
126
|
+
if not self._should_exclude(root_path / d, default_excludes, base_path)
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
for filename in filenames:
|
|
130
|
+
file_path = root_path / filename
|
|
131
|
+
total_scanned += 1
|
|
132
|
+
|
|
133
|
+
if self._should_exclude(file_path, default_excludes, base_path):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
if file_path.stat().st_size > 10_000_000:
|
|
138
|
+
continue
|
|
139
|
+
except:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
files.append(str(file_path))
|
|
143
|
+
|
|
144
|
+
if progress_callback and total_scanned % 100 == 0:
|
|
145
|
+
progress_callback(len(files), total_scanned)
|
|
146
|
+
|
|
147
|
+
return files
|
|
148
|
+
|
|
149
|
+
def _validate_url(self, url: str) -> bool:
|
|
150
|
+
"""Validate if URL has proper format"""
|
|
151
|
+
url = url.strip()
|
|
152
|
+
if not url:
|
|
153
|
+
return False
|
|
154
|
+
return url.startswith(('http://', 'https://'))
|
|
155
|
+
|
|
156
|
+
async def create_kb(self) -> bool:
|
|
157
|
+
"""Interactive knowledge base creation flow"""
|
|
158
|
+
console.print()
|
|
159
|
+
console.print(Panel(
|
|
160
|
+
"[cyan]📚 Create Knowledge Base[/cyan]\n\n"
|
|
161
|
+
"[white]Choose the type of knowledge base you want to create.[/white]",
|
|
162
|
+
border_style="cyan",
|
|
163
|
+
padding=(1, 2),
|
|
164
|
+
box=box.ROUNDED
|
|
165
|
+
))
|
|
166
|
+
console.print()
|
|
167
|
+
|
|
168
|
+
# Step 0: Choose KB type
|
|
169
|
+
console.print("[bold cyan]Step 1:[/bold cyan] Select knowledge base type")
|
|
170
|
+
console.print()
|
|
171
|
+
|
|
172
|
+
type_table = Table(
|
|
173
|
+
show_header=True,
|
|
174
|
+
header_style="bold magenta",
|
|
175
|
+
border_style="blue",
|
|
176
|
+
box=box.ROUNDED
|
|
177
|
+
)
|
|
178
|
+
type_table.add_column("#", style="dim", width=4)
|
|
179
|
+
type_table.add_column("Type", style="cyan", width=15)
|
|
180
|
+
type_table.add_column("Description", style="white", width=50)
|
|
181
|
+
|
|
182
|
+
type_table.add_row("1", "codebase", "Index a local directory/project with code files")
|
|
183
|
+
type_table.add_row("2", "docs", "Index documentation from URL(s)")
|
|
184
|
+
|
|
185
|
+
console.print(type_table)
|
|
186
|
+
console.print()
|
|
187
|
+
|
|
188
|
+
kb_type_input = Prompt.ask(
|
|
189
|
+
"[cyan]Select type[/cyan]",
|
|
190
|
+
choices=["1", "2", "codebase", "docs"],
|
|
191
|
+
default="1"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Normalize input
|
|
195
|
+
if kb_type_input in ["1", "codebase"]:
|
|
196
|
+
kb_type = "codebase"
|
|
197
|
+
return await self._create_codebase_kb()
|
|
198
|
+
else:
|
|
199
|
+
kb_type = "docs"
|
|
200
|
+
return await self._create_docs_kb()
|
|
201
|
+
|
|
202
|
+
async def _create_codebase_kb(self) -> bool:
|
|
203
|
+
"""Create a codebase knowledge base from local directory"""
|
|
204
|
+
console.print()
|
|
205
|
+
console.print("[dim]═" * console.width + "[/dim]")
|
|
206
|
+
console.print()
|
|
207
|
+
|
|
208
|
+
# Step 1: Get directory path
|
|
209
|
+
console.print("[bold cyan]Step 2:[/bold cyan] Specify the directory path")
|
|
210
|
+
console.print("[dim]Enter the full path to the directory you want to index[/dim]")
|
|
211
|
+
console.print()
|
|
212
|
+
|
|
213
|
+
path_input = Prompt.ask(
|
|
214
|
+
"[cyan]Directory path[/cyan]",
|
|
215
|
+
default="."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Normalize and validate path
|
|
219
|
+
try:
|
|
220
|
+
kb_path = self._normalize_path(path_input)
|
|
221
|
+
is_valid, error_msg = self._validate_path(kb_path)
|
|
222
|
+
|
|
223
|
+
if not is_valid:
|
|
224
|
+
console.print()
|
|
225
|
+
console.print(Panel(
|
|
226
|
+
f"[red]❌ Invalid Path[/red]\n\n{error_msg}",
|
|
227
|
+
border_style="red",
|
|
228
|
+
padding=(1, 2)
|
|
229
|
+
))
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
console.print(f"[green]✓[/green] Valid path: [cyan]{kb_path}[/cyan]")
|
|
233
|
+
console.print()
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
console.print()
|
|
237
|
+
console.print(Panel(
|
|
238
|
+
f"[red]❌ Error validating path[/red]\n\n{str(e)}",
|
|
239
|
+
border_style="red",
|
|
240
|
+
padding=(1, 2)
|
|
241
|
+
))
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
# Step 2: Get KB name
|
|
245
|
+
default_name = kb_path.name
|
|
246
|
+
console.print("[bold cyan]Step 3:[/bold cyan] Name your knowledge base")
|
|
247
|
+
console.print(f"[dim]Default: {default_name}[/dim]")
|
|
248
|
+
console.print()
|
|
249
|
+
|
|
250
|
+
kb_name = Prompt.ask(
|
|
251
|
+
"[cyan]Knowledge base name[/cyan]",
|
|
252
|
+
default=default_name
|
|
253
|
+
)
|
|
254
|
+
console.print()
|
|
255
|
+
|
|
256
|
+
# Step 3: Get description
|
|
257
|
+
console.print("[bold cyan]Step 4:[/bold cyan] Add a description [dim](optional)[/dim]")
|
|
258
|
+
kb_description = Prompt.ask(
|
|
259
|
+
"[cyan]Description[/cyan]",
|
|
260
|
+
default=""
|
|
261
|
+
)
|
|
262
|
+
console.print()
|
|
263
|
+
console.print("[dim]─" * console.width + "[/dim]")
|
|
264
|
+
console.print()
|
|
265
|
+
|
|
266
|
+
# Show summary
|
|
267
|
+
summary = Table(
|
|
268
|
+
show_header=False,
|
|
269
|
+
border_style="cyan",
|
|
270
|
+
box=box.ROUNDED,
|
|
271
|
+
padding=(0, 1)
|
|
272
|
+
)
|
|
273
|
+
summary.add_column(style="cyan bold", width=20)
|
|
274
|
+
summary.add_column(style="white")
|
|
275
|
+
|
|
276
|
+
summary.add_row("Type:", "codebase")
|
|
277
|
+
summary.add_row("Path:", str(kb_path))
|
|
278
|
+
summary.add_row("Name:", kb_name)
|
|
279
|
+
summary.add_row("Description:", kb_description or "[dim](none)[/dim]")
|
|
280
|
+
|
|
281
|
+
console.print(Panel(
|
|
282
|
+
summary,
|
|
283
|
+
title="[bold cyan]📋 Summary[/bold cyan]",
|
|
284
|
+
border_style="cyan",
|
|
285
|
+
padding=(1, 2)
|
|
286
|
+
))
|
|
287
|
+
console.print()
|
|
288
|
+
|
|
289
|
+
# Confirm creation
|
|
290
|
+
if not Confirm.ask("[cyan]Create this knowledge base?[/cyan]", default=True):
|
|
291
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
console.print()
|
|
295
|
+
console.print("[dim]─" * console.width + "[/dim]")
|
|
296
|
+
console.print()
|
|
297
|
+
|
|
298
|
+
# Collect files
|
|
299
|
+
console.print("[cyan]🔍 Scanning directory...[/cyan]")
|
|
300
|
+
console.print()
|
|
301
|
+
|
|
302
|
+
files = []
|
|
303
|
+
try:
|
|
304
|
+
with Progress(
|
|
305
|
+
SpinnerColumn(),
|
|
306
|
+
TextColumn("[progress.description]{task.description}"),
|
|
307
|
+
transient=True
|
|
308
|
+
) as progress:
|
|
309
|
+
task = progress.add_task("Scanning files...", total=None)
|
|
310
|
+
|
|
311
|
+
def progress_callback(found, scanned):
|
|
312
|
+
progress.update(task, description=f"Found {found} files (scanned {scanned})")
|
|
313
|
+
|
|
314
|
+
files = self._collect_files(kb_path, progress_callback)
|
|
315
|
+
|
|
316
|
+
if not files:
|
|
317
|
+
console.print()
|
|
318
|
+
console.print(Panel(
|
|
319
|
+
"[yellow]⚠️ No files found[/yellow]\n\n"
|
|
320
|
+
"The directory appears to be empty or all files were excluded.",
|
|
321
|
+
border_style="yellow",
|
|
322
|
+
padding=(1, 2)
|
|
323
|
+
))
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
console.print(f"[green]✓[/green] Found [cyan]{len(files)}[/cyan] files")
|
|
327
|
+
console.print()
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
console.print()
|
|
331
|
+
console.print(Panel(
|
|
332
|
+
f"[red]❌ Error scanning directory[/red]\n\n{str(e)}",
|
|
333
|
+
border_style="red",
|
|
334
|
+
padding=(1, 2)
|
|
335
|
+
))
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
# Prepare upload data
|
|
339
|
+
kb_id = str(uuid.uuid4())
|
|
340
|
+
request_id = ''.join(str(uuid.uuid4()).split('-'))[:22]
|
|
341
|
+
|
|
342
|
+
upload_data = {
|
|
343
|
+
"id": kb_id,
|
|
344
|
+
"isAutoIndexed": False,
|
|
345
|
+
"name": kb_name,
|
|
346
|
+
"description": kb_description,
|
|
347
|
+
"source": "LOCAL",
|
|
348
|
+
"scope": "personal",
|
|
349
|
+
"syncConfig": {
|
|
350
|
+
"enabled": False,
|
|
351
|
+
"lastSynced": int(time.time() * 1000)
|
|
352
|
+
},
|
|
353
|
+
"status": "draft",
|
|
354
|
+
"progress": {
|
|
355
|
+
"status": "",
|
|
356
|
+
"message": "",
|
|
357
|
+
"progress": 0
|
|
358
|
+
},
|
|
359
|
+
"dateCreated": int(time.time() * 1000),
|
|
360
|
+
"dateSynced": None,
|
|
361
|
+
"dateUpdated": None,
|
|
362
|
+
"can_sync": False,
|
|
363
|
+
"can_upload": False,
|
|
364
|
+
"cloud_id": "",
|
|
365
|
+
"type": "codebase",
|
|
366
|
+
"metadata": {
|
|
367
|
+
"path": str(kb_path),
|
|
368
|
+
"files": files
|
|
369
|
+
},
|
|
370
|
+
"request_id": request_id
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# Upload
|
|
374
|
+
console.print("[cyan]📤 Uploading to server...[/cyan]")
|
|
375
|
+
console.print()
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
await self._upload_via_socket(upload_data, "codebase")
|
|
379
|
+
return True
|
|
380
|
+
except Exception as e:
|
|
381
|
+
console.print()
|
|
382
|
+
console.print(Panel(
|
|
383
|
+
f"[red]❌ Upload failed[/red]\n\n{str(e)}",
|
|
384
|
+
border_style="red",
|
|
385
|
+
padding=(1, 2)
|
|
386
|
+
))
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
async def _create_docs_kb(self) -> bool:
|
|
390
|
+
"""Create a docs knowledge base from URLs"""
|
|
391
|
+
console.print()
|
|
392
|
+
console.print("[dim]═" * console.width + "[/dim]")
|
|
393
|
+
console.print()
|
|
394
|
+
|
|
395
|
+
# Step 1: Get URLs
|
|
396
|
+
console.print("[bold cyan]Step 2:[/bold cyan] Provide documentation URL(s)")
|
|
397
|
+
console.print("[dim]Enter one or more URLs (comma-separated)[/dim]")
|
|
398
|
+
console.print("[dim]Example: https://docs.example.com, https://guide.example.com[/dim]")
|
|
399
|
+
console.print()
|
|
400
|
+
|
|
401
|
+
urls_input = Prompt.ask("[cyan]Documentation URL(s)[/cyan]")
|
|
402
|
+
|
|
403
|
+
# Parse and validate URLs
|
|
404
|
+
urls = [url.strip() for url in urls_input.split(',')]
|
|
405
|
+
valid_urls = []
|
|
406
|
+
invalid_urls = []
|
|
407
|
+
|
|
408
|
+
for url in urls:
|
|
409
|
+
if self._validate_url(url):
|
|
410
|
+
valid_urls.append(url)
|
|
411
|
+
else:
|
|
412
|
+
invalid_urls.append(url)
|
|
413
|
+
|
|
414
|
+
if invalid_urls:
|
|
415
|
+
console.print()
|
|
416
|
+
console.print(Panel(
|
|
417
|
+
f"[yellow]⚠️ Invalid URL(s) found:[/yellow]\n\n" +
|
|
418
|
+
"\n".join([f"• {url}" for url in invalid_urls]) +
|
|
419
|
+
"\n\n[white]URLs must start with http:// or https://[/white]",
|
|
420
|
+
border_style="yellow",
|
|
421
|
+
padding=(1, 2)
|
|
422
|
+
))
|
|
423
|
+
|
|
424
|
+
if not valid_urls:
|
|
425
|
+
console.print()
|
|
426
|
+
console.print(Panel(
|
|
427
|
+
"[red]❌ No valid URLs provided[/red]",
|
|
428
|
+
border_style="red",
|
|
429
|
+
padding=(1, 2)
|
|
430
|
+
))
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
console.print(f"[green]✓[/green] Valid URLs: [cyan]{len(valid_urls)}[/cyan]")
|
|
434
|
+
console.print()
|
|
435
|
+
|
|
436
|
+
# Step 2: Get KB name
|
|
437
|
+
console.print("[bold cyan]Step 3:[/bold cyan] Name your knowledge base")
|
|
438
|
+
console.print()
|
|
439
|
+
|
|
440
|
+
kb_name = Prompt.ask("[cyan]Knowledge base name[/cyan]")
|
|
441
|
+
console.print()
|
|
442
|
+
|
|
443
|
+
# Step 3: Get description
|
|
444
|
+
console.print("[bold cyan]Step 4:[/bold cyan] Add a description [dim](optional)[/dim]")
|
|
445
|
+
kb_description = Prompt.ask(
|
|
446
|
+
"[cyan]Description[/cyan]",
|
|
447
|
+
default=""
|
|
448
|
+
)
|
|
449
|
+
console.print()
|
|
450
|
+
console.print("[dim]─" * console.width + "[/dim]")
|
|
451
|
+
console.print()
|
|
452
|
+
|
|
453
|
+
# Show summary
|
|
454
|
+
summary = Table(
|
|
455
|
+
show_header=False,
|
|
456
|
+
border_style="cyan",
|
|
457
|
+
box=box.ROUNDED,
|
|
458
|
+
padding=(0, 1)
|
|
459
|
+
)
|
|
460
|
+
summary.add_column(style="cyan bold", width=20)
|
|
461
|
+
summary.add_column(style="white")
|
|
462
|
+
|
|
463
|
+
summary.add_row("Type:", "docs")
|
|
464
|
+
summary.add_row("URLs:", str(len(valid_urls)))
|
|
465
|
+
summary.add_row("Name:", kb_name)
|
|
466
|
+
summary.add_row("Description:", kb_description or "[dim](none)[/dim]")
|
|
467
|
+
|
|
468
|
+
console.print(Panel(
|
|
469
|
+
summary,
|
|
470
|
+
title="[bold cyan]📋 Summary[/bold cyan]",
|
|
471
|
+
border_style="cyan",
|
|
472
|
+
padding=(1, 2)
|
|
473
|
+
))
|
|
474
|
+
console.print()
|
|
475
|
+
|
|
476
|
+
# Show URLs
|
|
477
|
+
if valid_urls:
|
|
478
|
+
console.print("[cyan]URLs to index:[/cyan]")
|
|
479
|
+
for i, url in enumerate(valid_urls, 1):
|
|
480
|
+
console.print(f" [dim]{i}.[/dim] {url}")
|
|
481
|
+
console.print()
|
|
482
|
+
|
|
483
|
+
# Confirm creation
|
|
484
|
+
if not Confirm.ask("[cyan]Create this knowledge base?[/cyan]", default=True):
|
|
485
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
console.print()
|
|
489
|
+
console.print("[dim]─" * console.width + "[/dim]")
|
|
490
|
+
console.print()
|
|
491
|
+
|
|
492
|
+
# Prepare upload data
|
|
493
|
+
kb_id = str(uuid.uuid4())
|
|
494
|
+
request_id = ''.join(str(uuid.uuid4()).split('-'))[:22]
|
|
495
|
+
|
|
496
|
+
upload_data = {
|
|
497
|
+
"id": kb_id,
|
|
498
|
+
"isAutoIndexed": False,
|
|
499
|
+
"name": kb_name,
|
|
500
|
+
"description": kb_description,
|
|
501
|
+
"source": "DOCS",
|
|
502
|
+
"scope": "personal",
|
|
503
|
+
"syncConfig": {
|
|
504
|
+
"enabled": False,
|
|
505
|
+
"lastSynced": int(time.time() * 1000)
|
|
506
|
+
},
|
|
507
|
+
"status": "draft",
|
|
508
|
+
"progress": {
|
|
509
|
+
"status": "",
|
|
510
|
+
"message": "",
|
|
511
|
+
"progress": 0
|
|
512
|
+
},
|
|
513
|
+
"dateCreated": int(time.time() * 1000),
|
|
514
|
+
"dateSynced": None,
|
|
515
|
+
"dateUpdated": None,
|
|
516
|
+
"can_sync": False,
|
|
517
|
+
"can_upload": False,
|
|
518
|
+
"cloud_id": "",
|
|
519
|
+
"type": "docs",
|
|
520
|
+
"metadata": {
|
|
521
|
+
"urls": valid_urls
|
|
522
|
+
},
|
|
523
|
+
"request_id": request_id
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# Upload
|
|
527
|
+
console.print("[cyan]📤 Processing documentation...[/cyan]")
|
|
528
|
+
console.print()
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
await self._upload_via_socket(upload_data, "docs")
|
|
532
|
+
return True
|
|
533
|
+
except Exception as e:
|
|
534
|
+
console.print()
|
|
535
|
+
console.print(Panel(
|
|
536
|
+
f"[red]❌ Processing failed[/red]\n\n{str(e)}",
|
|
537
|
+
border_style="red",
|
|
538
|
+
padding=(1, 2)
|
|
539
|
+
))
|
|
540
|
+
return False
|
|
541
|
+
|
|
542
|
+
async def _upload_via_socket(self, upload_data: dict, kb_type: str):
|
|
543
|
+
"""Upload knowledge base via SocketIO"""
|
|
544
|
+
sio = socketio.AsyncClient()
|
|
545
|
+
upload_complete = False
|
|
546
|
+
upload_error = None
|
|
547
|
+
|
|
548
|
+
@sio.on("upload:success")
|
|
549
|
+
def on_success(data):
|
|
550
|
+
nonlocal upload_complete
|
|
551
|
+
upload_complete = True
|
|
552
|
+
console.print()
|
|
553
|
+
|
|
554
|
+
if kb_type == "codebase":
|
|
555
|
+
file_count = len(upload_data['metadata']['files'])
|
|
556
|
+
detail_line = f"[white]Files:[/white] [cyan]{file_count}[/cyan]"
|
|
557
|
+
else:
|
|
558
|
+
url_count = len(upload_data['metadata']['urls'])
|
|
559
|
+
detail_line = f"[white]URLs:[/white] [cyan]{url_count}[/cyan]"
|
|
560
|
+
|
|
561
|
+
console.print(Panel(
|
|
562
|
+
"[green]✓ Knowledge base created successfully![/green]\n\n"
|
|
563
|
+
f"[white]Name:[/white] [cyan]{upload_data['name']}[/cyan]\n"
|
|
564
|
+
f"{detail_line}\n\n"
|
|
565
|
+
"[dim]Use /listkb to view all knowledge bases[/dim]",
|
|
566
|
+
border_style="green",
|
|
567
|
+
title="[bold green]Success[/bold green]",
|
|
568
|
+
padding=(1, 2)
|
|
569
|
+
))
|
|
570
|
+
|
|
571
|
+
@sio.on("upload:error")
|
|
572
|
+
def on_error(data):
|
|
573
|
+
nonlocal upload_complete, upload_error
|
|
574
|
+
upload_complete = True
|
|
575
|
+
upload_error = data.get("message", "Unknown error")
|
|
576
|
+
console.print()
|
|
577
|
+
console.print(Panel(
|
|
578
|
+
f"[red]❌ Upload failed[/red]\n\n{upload_error}",
|
|
579
|
+
border_style="red",
|
|
580
|
+
padding=(1, 2)
|
|
581
|
+
))
|
|
582
|
+
|
|
583
|
+
try:
|
|
584
|
+
# Connect to server
|
|
585
|
+
await sio.connect(self.socket_url)
|
|
586
|
+
|
|
587
|
+
# Send upload event
|
|
588
|
+
await sio.emit("upload", upload_data)
|
|
589
|
+
|
|
590
|
+
# Wait for completion with progress indicator
|
|
591
|
+
with Progress(
|
|
592
|
+
SpinnerColumn(),
|
|
593
|
+
TextColumn("[progress.description]{task.description}"),
|
|
594
|
+
transient=True
|
|
595
|
+
) as progress:
|
|
596
|
+
task = progress.add_task(
|
|
597
|
+
f"Processing {kb_type}..." if kb_type == "docs" else "Uploading files...",
|
|
598
|
+
total=None
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
timeout = 6000 # 5 minutes
|
|
602
|
+
start_time = time.time()
|
|
603
|
+
|
|
604
|
+
while not upload_complete:
|
|
605
|
+
if time.time() - start_time > timeout:
|
|
606
|
+
raise TimeoutError("Upload timed out after 5 minutes")
|
|
607
|
+
await sio.sleep(0.5)
|
|
608
|
+
|
|
609
|
+
if upload_error:
|
|
610
|
+
raise Exception(upload_error)
|
|
611
|
+
|
|
612
|
+
finally:
|
|
613
|
+
await sio.disconnect()
|
|
614
|
+
|
|
615
|
+
async def delete_kb(self) -> bool:
|
|
616
|
+
"""Interactive knowledge base deletion"""
|
|
617
|
+
console.print()
|
|
618
|
+
console.print(Panel(
|
|
619
|
+
"[red]🗑️ Delete Knowledge Base[/red]\n\n"
|
|
620
|
+
"[yellow]Warning: This action cannot be undone![/yellow]",
|
|
621
|
+
border_style="red",
|
|
622
|
+
padding=(1, 2),
|
|
623
|
+
box=box.ROUNDED
|
|
624
|
+
))
|
|
625
|
+
console.print()
|
|
626
|
+
|
|
627
|
+
# Get available KBs
|
|
628
|
+
try:
|
|
629
|
+
kbs = self.client.list_kbs()
|
|
630
|
+
|
|
631
|
+
if not kbs:
|
|
632
|
+
console.print("[yellow]No knowledge bases found.[/yellow]")
|
|
633
|
+
return False
|
|
634
|
+
|
|
635
|
+
# Show available KBs
|
|
636
|
+
console.print("[cyan]Available knowledge bases:[/cyan]")
|
|
637
|
+
console.print()
|
|
638
|
+
|
|
639
|
+
table = Table(
|
|
640
|
+
show_header=True,
|
|
641
|
+
header_style="bold magenta",
|
|
642
|
+
border_style="blue",
|
|
643
|
+
box=box.ROUNDED
|
|
644
|
+
)
|
|
645
|
+
table.add_column("#", style="dim", width=4)
|
|
646
|
+
table.add_column("Name", style="cyan", width=25)
|
|
647
|
+
table.add_column("Type", style="yellow", width=12)
|
|
648
|
+
table.add_column("Files/Items", style="green", width=12)
|
|
649
|
+
|
|
650
|
+
for idx, kb in enumerate(kbs, 1):
|
|
651
|
+
kb_name = kb.get('name', '')
|
|
652
|
+
kb_type = kb.get('type', 'unknown')
|
|
653
|
+
|
|
654
|
+
# Get count based on type
|
|
655
|
+
metadata = kb.get('metadata', {})
|
|
656
|
+
if kb_type == "codebase":
|
|
657
|
+
count = len(metadata.get('files', [])) if 'files' in metadata else '-'
|
|
658
|
+
elif kb_type == "docs":
|
|
659
|
+
count = len(metadata.get('urls', [])) if 'urls' in metadata else '-'
|
|
660
|
+
else:
|
|
661
|
+
count = '-'
|
|
662
|
+
|
|
663
|
+
table.add_row(str(idx), kb_name, kb_type, str(count))
|
|
664
|
+
|
|
665
|
+
console.print(table)
|
|
666
|
+
console.print()
|
|
667
|
+
|
|
668
|
+
except Exception as e:
|
|
669
|
+
console.print(f"[red]Error listing knowledge bases: {e}[/red]")
|
|
670
|
+
return False
|
|
671
|
+
|
|
672
|
+
# Get KB to delete
|
|
673
|
+
kb_input = Prompt.ask(
|
|
674
|
+
"[cyan]Enter knowledge base name or number[/cyan]"
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Find KB
|
|
678
|
+
selected_kb = None
|
|
679
|
+
|
|
680
|
+
try:
|
|
681
|
+
idx = int(kb_input) - 1
|
|
682
|
+
if 0 <= idx < len(kbs):
|
|
683
|
+
selected_kb = kbs[idx]
|
|
684
|
+
except ValueError:
|
|
685
|
+
for kb in kbs:
|
|
686
|
+
if kb.get('name', '').lower() == kb_input.lower():
|
|
687
|
+
selected_kb = kb
|
|
688
|
+
break
|
|
689
|
+
|
|
690
|
+
if not selected_kb:
|
|
691
|
+
console.print()
|
|
692
|
+
console.print(f"[red]❌ Knowledge base not found: {kb_input}[/red]")
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
kb_name = selected_kb.get('name')
|
|
696
|
+
kb_id = selected_kb.get('id')
|
|
697
|
+
|
|
698
|
+
console.print()
|
|
699
|
+
console.print(f"[yellow]⚠️ You are about to delete:[/yellow] [cyan]{kb_name}[/cyan]")
|
|
700
|
+
console.print()
|
|
701
|
+
|
|
702
|
+
# Confirm deletion
|
|
703
|
+
if not Confirm.ask(
|
|
704
|
+
f"[red]Are you sure you want to delete '{kb_name}'?[/red]",
|
|
705
|
+
default=False
|
|
706
|
+
):
|
|
707
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
708
|
+
return False
|
|
709
|
+
|
|
710
|
+
# Delete KB
|
|
711
|
+
console.print()
|
|
712
|
+
console.print("[cyan]🗑️ Deleting knowledge base...[/cyan]")
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
716
|
+
response = await client.post(
|
|
717
|
+
f"{self.api_url}/delete_kb",
|
|
718
|
+
json={"kbid": kb_id},
|
|
719
|
+
headers={"Content-Type": "application/json"}
|
|
720
|
+
)
|
|
721
|
+
response.raise_for_status()
|
|
722
|
+
result = response.json()
|
|
723
|
+
|
|
724
|
+
if result.get("status") == "success":
|
|
725
|
+
console.print()
|
|
726
|
+
console.print(Panel(
|
|
727
|
+
f"[green]✓ Successfully deleted '{kb_name}'[/green]",
|
|
728
|
+
border_style="green",
|
|
729
|
+
padding=(1, 2)
|
|
730
|
+
))
|
|
731
|
+
return True
|
|
732
|
+
else:
|
|
733
|
+
error_msg = result.get("message", "Unknown error")
|
|
734
|
+
console.print()
|
|
735
|
+
console.print(Panel(
|
|
736
|
+
f"[red]❌ Deletion failed[/red]\n\n{error_msg}",
|
|
737
|
+
border_style="red",
|
|
738
|
+
padding=(1, 2)
|
|
739
|
+
))
|
|
740
|
+
return False
|
|
741
|
+
|
|
742
|
+
except Exception as e:
|
|
743
|
+
console.print()
|
|
744
|
+
console.print(Panel(
|
|
745
|
+
f"[red]❌ Error deleting knowledge base[/red]\n\n{str(e)}",
|
|
746
|
+
border_style="red",
|
|
747
|
+
padding=(1, 2)
|
|
748
|
+
))
|
|
749
|
+
return False
|