1use anyhow::Result;
7use clap::CommandFactory;
8use clap_complete::{generate, Shell};
9use std::io;
10
11use crate::cli::{Cli, CompletionShell};
12use crate::config::Config;
13
14pub async fn generate_completions(shell: CompletionShell) -> Result<()> {
16 let mut cmd = Cli::command();
17 let shell_type = match shell {
18 CompletionShell::Bash => Shell::Bash,
19 CompletionShell::Zsh => Shell::Zsh,
20 CompletionShell::Fish => Shell::Fish,
21 CompletionShell::PowerShell => Shell::PowerShell,
22 CompletionShell::Elvish => Shell::Elvish,
23 };
24
25 generate(shell_type, &mut cmd, "lc", &mut io::stdout());
27
28 match shell {
30 CompletionShell::Bash => generate_bash_dynamic_completions(),
31 CompletionShell::Zsh => generate_zsh_dynamic_completions(),
32 CompletionShell::Fish => generate_fish_dynamic_completions(),
33 _ => {
34 eprintln!(
35 "Note: Dynamic completions for providers are not yet supported for {:?}",
36 shell
37 );
38 eprintln!("Basic command completions have been generated.");
39 }
40 }
41
42 Ok(())
43}
44
45fn generate_bash_dynamic_completions() {
47 println!(
48 r#"
49# Dynamic completion functions for lc (Bash)
50_lc_complete_providers() {{
51 local providers
52 providers=$(lc providers list 2>/dev/null | grep " •" | awk '{{print $2}}' 2>/dev/null || echo "")
53 COMPREPLY=($(compgen -W "$providers" -- "${{COMP_WORDS[COMP_CWORD]}}"))
54}}
55
56_lc_complete_models() {{
57 local models provider
58 # Check if a provider was specified with -p or --provider
59 for ((i=1; i<COMP_CWORD; i++)); do
60 if [[ "${{COMP_WORDS[i]}}" == "-p" || "${{COMP_WORDS[i]}}" == "--provider" ]]; then
61 provider="${{COMP_WORDS[i+1]}}"
62 break
63 elif [[ "${{COMP_WORDS[i]}}" =~ ^--provider= ]]; then
64 provider="${{COMP_WORDS[i]#--provider=}}"
65 break
66 elif [[ "${{COMP_WORDS[i]}}" =~ ^-p.+ ]]; then
67 provider="${{COMP_WORDS[i]#-p}}"
68 break
69 fi
70 done
71
72 if [[ -n "$provider" ]]; then
73 # Get models for specific provider (extract full model name including colons)
74 models=$(lc providers models "$provider" 2>/dev/null | grep " •" | awk -F' ' '{{gsub(/^ • /, "", $0); gsub(/ \(.*$/, "", $0); gsub(/ \[.*$/, "", $0); print $1}}' 2>/dev/null || echo "")
75 else
76 # Get all models in provider:model format
77 models=$(lc models 2>/dev/null | awk '
78 /^[a-zA-Z0-9_-]+:$/ {{ provider = substr($0, 1, length($0)-1) }}
79 /^ •/ {{
80 gsub(/^ • /, "")
81 gsub(/ \(.*$/, "")
82 gsub(/ \[.*$/, "")
83 if (provider != "") print provider ":" $0
84 }}
85 ' 2>/dev/null || echo "")
86 fi
87 COMPREPLY=($(compgen -W "$models" -- "${{COMP_WORDS[COMP_CWORD]}}"))
88}}
89
90_lc_complete_vectordbs() {{
91 local vectordbs
92 vectordbs=$(lc vectors list 2>/dev/null | grep " •" | awk '{{print $2}}' 2>/dev/null || echo "")
93 COMPREPLY=($(compgen -W "$vectordbs" -- "${{COMP_WORDS[COMP_CWORD]}}"))
94}}
95
96# Enhanced completion function with alias support
97_lc_enhanced() {{
98 local cur prev opts cmd
99 COMPREPLY=()
100 cur="${{COMP_WORDS[COMP_CWORD]}}"
101 prev="${{COMP_WORDS[COMP_CWORD-1]}}"
102
103 # Handle command aliases by expanding them
104 if [[ COMP_CWORD -ge 1 ]]; then
105 cmd="${{COMP_WORDS[1]}}"
106 case "$cmd" in
107 p)
108 COMP_WORDS[1]="providers"
109 ;;
110 k)
111 COMP_WORDS[1]="keys"
112 ;;
113 l)
114 COMP_WORDS[1]="logs"
115 ;;
116 co)
117 COMP_WORDS[1]="config"
118 ;;
119 c)
120 COMP_WORDS[1]="chat"
121 ;;
122 m)
123 COMP_WORDS[1]="models"
124 ;;
125 a)
126 COMP_WORDS[1]="alias"
127 ;;
128 t)
129 COMP_WORDS[1]="templates"
130 ;;
131 pr)
132 COMP_WORDS[1]="proxy"
133 ;;
134 e)
135 COMP_WORDS[1]="embed"
136 ;;
137 s)
138 COMP_WORDS[1]="similar"
139 ;;
140 v)
141 COMP_WORDS[1]="vectors"
142 ;;
143 w)
144 COMP_WORDS[1]="web-chat-proxy"
145 ;;
146 sy)
147 COMP_WORDS[1]="sync"
148 ;;
149 se)
150 COMP_WORDS[1]="search"
151 ;;
152 img)
153 COMP_WORDS[1]="image"
154 ;;
155 dump)
156 COMP_WORDS[1]="dump-metadata"
157 ;;
158 esac
159 fi
160
161 case "$prev" in
162 -p|--provider)
163 _lc_complete_providers
164 return 0
165 ;;
166 -m|--model)
167 _lc_complete_models
168 return 0
169 ;;
170 -v|--vectordb|--database)
171 _lc_complete_vectordbs
172 return 0
173 ;;
174 esac
175
176 # Fall back to default completion
177 _lc "$@"
178}}
179
180# Register the enhanced completion
181complete -F _lc_enhanced lc
182
183# Instructions for setup
184# Add the above to your ~/.bashrc or ~/.bash_completion to enable dynamic completions
185# Then run: source ~/.bashrc
186"#
187 );
188}
189
190fn generate_zsh_dynamic_completions() {
192 println!(
193 r#"
194# Dynamic completion functions for lc (Zsh)
195_lc_providers() {{
196 local providers
197 providers=($(lc providers list 2>/dev/null | grep " •" | awk '{{print $2}}' 2>/dev/null || echo ""))
198 _describe 'providers' providers
199}}
200
201_lc_models() {{
202 local models provider
203 # Check if a provider was specified with -p or --provider in the current command line
204 local -a words
205 words=(${{(z)BUFFER}})
206
207 for ((i=1; i<=${{#words}}; i++)); do
208 if [[ "${{words[i]}}" == "-p" || "${{words[i]}}" == "--provider" ]]; then
209 provider="${{words[i+1]}}"
210 break
211 elif [[ "${{words[i]}}" =~ ^--provider= ]]; then
212 provider="${{words[i]#--provider=}}"
213 break
214 elif [[ "${{words[i]}}" =~ ^-p.+ ]]; then
215 provider="${{words[i]#-p}}"
216 break
217 fi
218 done
219
220 if [[ -n "$provider" ]]; then
221 # Get models for specific provider (extract full model name including colons)
222 models=($(lc providers models "$provider" 2>/dev/null | grep " •" | awk -F' ' '{{gsub(/^ • /, "", $0); gsub(/ \(.*$/, "", $0); gsub(/ \[.*$/, "", $0); print $1}}' 2>/dev/null || echo ""))
223 # For provider-specific models, just use the model names directly
224 _describe 'models' models
225 else
226 # Get all models in provider:model format
227 local raw_models
228 raw_models=($(lc models 2>/dev/null | awk '
229 /^[a-zA-Z0-9_-]+:$/ {{ provider = substr($0, 1, length($0)-1) }}
230 /^ •/ {{
231 gsub(/^ • /, "")
232 gsub(/ \(.*$/, "")
233 gsub(/ \[.*$/, "")
234 if (provider != "") print provider ":" $0
235 }}
236 ' 2>/dev/null || echo ""))
237
238 # Use compadd with proper display format: "provider -- model"
239 local -a completions descriptions
240 for model in $raw_models; do
241 local provider_part="${{model%%:*}}"
242 local model_part="${{model#*:}}"
243 completions+=("$model")
244 descriptions+=("$provider_part -- $model_part")
245 done
246
247 if [[ ${{#completions}} -gt 0 ]]; then
248 compadd -d descriptions -a completions
249 fi
250 fi
251}}
252
253_lc_vectordbs() {{
254 local vectordbs
255 vectordbs=($(lc vectors list 2>/dev/null | grep " •" | awk '{{print $2}}' 2>/dev/null || echo ""))
256 _describe 'vectordbs' vectordbs
257}}
258
259# Override the default completion to use our dynamic functions
260# This replaces _default with our custom functions in the generated completion
261if (( $+functions[_lc] )); then
262 # Modify the existing _lc function to use our dynamic completions
263 eval "$(declare -f _lc | sed \
264 -e "s/:PROVIDER:_default/:PROVIDER:_lc_providers/g" \
265 -e "s/:MODEL:_default/:MODEL:_lc_models/g" \
266 -e "s/:VECTORDB:_default/:VECTORDB:_lc_vectordbs/g" \
267 -e "s/:DATABASE:_default/:DATABASE:_lc_vectordbs/g")"
268fi
269
270# Custom wrapper function to handle command aliases
271_lc_with_aliases() {{
272 local context curcontext="$curcontext" state line
273 typeset -A opt_args
274 typeset -a _arguments_options
275 local ret=1
276
277 if is-at-least 5.2; then
278 _arguments_options=(-s -S -C)
279 else
280 _arguments_options=(-s -C)
281 fi
282
283 # First, let the original _lc function handle most of the work
284 _lc "$@"
285 ret=$?
286
287 # If we're in a command context and have an alias, handle it specially
288 if [[ $state == "lc" && -n $line[2] ]]; then
289 case $line[2] in
290 (p)
291 # Redirect 'p' alias to 'providers' subcommand completion
292 words=("providers" "${{words[@]:2}}")
293 (( CURRENT -= 1 ))
294 curcontext="${{curcontext%:*:*}}:lc-command-providers:"
295
296 # Handle special case for 'lc p m <TAB>' (providers models command)
297 if [[ ${{#words}} -ge 3 && "${{words[2]}}" == "m" ]]; then
298 # This is 'lc p m <TAB>' - should complete with provider names
299 _lc_providers
300 ret=$?
301 elif [[ ${{#words}} -ge 3 && "${{words[2]}}" == "models" ]]; then
302 # This is 'lc p models <TAB>' - should complete with provider names
303 _lc_providers
304 ret=$?
305 else
306 _lc__providers_commands
307 ret=$?
308 fi
309 ;;
310 (k)
311 # Redirect 'k' alias to 'keys' subcommand completion
312 words=("keys" "${{words[@]:2}}")
313 (( CURRENT -= 1 ))
314 curcontext="${{curcontext%:*:*}}:lc-command-keys:"
315 _lc__keys_commands
316 ret=$?
317 ;;
318 (l)
319 # Redirect 'l' alias to 'logs' subcommand completion
320 words=("logs" "${{words[@]:2}}")
321 (( CURRENT -= 1 ))
322 curcontext="${{curcontext%:*:*}}:lc-command-logs:"
323 _lc__logs_commands
324 ret=$?
325 ;;
326 (co)
327 # Redirect 'co' alias to 'config' subcommand completion
328 words=("config" "${{words[@]:2}}")
329 (( CURRENT -= 1 ))
330 curcontext="${{curcontext%:*:*}}:lc-command-config:"
331 _lc__config_commands
332 ret=$?
333 ;;
334 (c)
335 # Redirect 'c' alias to 'chat' subcommand completion
336 words=("chat" "${{words[@]:2}}")
337 (( CURRENT -= 1 ))
338 curcontext="${{curcontext%:*:*}}:lc-command-chat:"
339 # Chat command has no subcommands, so just complete its options
340 _arguments "${{_arguments_options[@]}}" : \
341 '-m+[Model to use for the chat]:MODEL:_lc_models' \
342 '--model=[Model to use for the chat]:MODEL:_lc_models' \
343 '-p+[Provider to use for the chat]:PROVIDER:_lc_providers' \
344 '--provider=[Provider to use for the chat]:PROVIDER:_lc_providers' \
345 '--cid=[Chat ID to use or continue]:CHAT_ID:_default' \
346 '-t+[Include tools from MCP server(s)]:TOOLS:_default' \
347 '--tools=[Include tools from MCP server(s)]:TOOLS:_default' \
348 '-v+[Vector database name for RAG]:DATABASE:_lc_vectordbs' \
349 '--vectordb=[Vector database name for RAG]:DATABASE:_lc_vectordbs' \
350 '-d[Enable debug/verbose logging]' \
351 '--debug[Enable debug/verbose logging]' \
352 '*-i+[Attach image(s) to the chat]:IMAGES:_default' \
353 '*--image=[Attach image(s) to the chat]:IMAGES:_default' \
354 '-h[Print help]' \
355 '--help[Print help]'
356 ret=$?
357 ;;
358 (m)
359 # Redirect 'm' alias to 'models' subcommand completion
360 words=("models" "${{words[@]:2}}")
361 (( CURRENT -= 1 ))
362 curcontext="${{curcontext%:*:*}}:lc-command-models:"
363 _lc__models_commands
364 ret=$?
365 ;;
366 (a)
367 # Redirect 'a' alias to 'alias' subcommand completion
368 words=("alias" "${{words[@]:2}}")
369 (( CURRENT -= 1 ))
370 curcontext="${{curcontext%:*:*}}:lc-command-alias:"
371 _lc__alias_commands
372 ret=$?
373 ;;
374 (t)
375 # Redirect 't' alias to 'templates' subcommand completion
376 words=("templates" "${{words[@]:2}}")
377 (( CURRENT -= 1 ))
378 curcontext="${{curcontext%:*:*}}:lc-command-templates:"
379 _lc__templates_commands
380 ret=$?
381 ;;
382 (pr)
383 # Redirect 'pr' alias to 'proxy' subcommand completion
384 words=("proxy" "${{words[@]:2}}")
385 (( CURRENT -= 1 ))
386 curcontext="${{curcontext%:*:*}}:lc-command-proxy:"
387 # Proxy command has no subcommands, so just complete its options
388 _arguments "${{_arguments_options[@]}}" : \
389 '-p+[Port to listen on]:PORT:_default' \
390 '--port=[Port to listen on]:PORT:_default' \
391 '--host=[Host to bind to]:HOST:_default' \
392 '--provider=[Filter by provider]:PROVIDER:_lc_providers' \
393 '-m+[Filter by specific model]:MODEL:_lc_models' \
394 '--model=[Filter by specific model]:MODEL:_lc_models' \
395 '-k+[API key for authentication]:API_KEY:_default' \
396 '--key=[API key for authentication]:API_KEY:_default' \
397 '-g[Generate a random API key]' \
398 '--generate-key[Generate a random API key]' \
399 '-h[Print help]' \
400 '--help[Print help]'
401 ret=$?
402 ;;
403 (e)
404 # Redirect 'e' alias to 'embed' subcommand completion
405 words=("embed" "${{words[@]:2}}")
406 (( CURRENT -= 1 ))
407 curcontext="${{curcontext%:*:*}}:lc-command-embed:"
408 # Embed command has no subcommands, so just complete its options
409 _arguments "${{_arguments_options[@]}}" : \
410 '-m+[Model to use for embeddings]:MODEL:_lc_models' \
411 '--model=[Model to use for embeddings]:MODEL:_lc_models' \
412 '-p+[Provider to use for embeddings]:PROVIDER:_lc_providers' \
413 '--provider=[Provider to use for embeddings]:PROVIDER:_lc_providers' \
414 '-v+[Vector database name to store embeddings]:DATABASE:_lc_vectordbs' \
415 '--vectordb=[Vector database name to store embeddings]:DATABASE:_lc_vectordbs' \
416 '*-f+[Files to embed]:FILES:_files' \
417 '*--files=[Files to embed]:FILES:_files' \
418 '-d[Enable debug/verbose logging]' \
419 '--debug[Enable debug/verbose logging]' \
420 '-h[Print help]' \
421 '--help[Print help]' \
422 '::text -- Text to embed:_default'
423 ret=$?
424 ;;
425 (s)
426 # Redirect 's' alias to 'similar' subcommand completion
427 words=("similar" "${{words[@]:2}}")
428 (( CURRENT -= 1 ))
429 curcontext="${{curcontext%:*:*}}:lc-command-similar:"
430 # Similar command has no subcommands, so just complete its options
431 _arguments "${{_arguments_options[@]}}" : \
432 '-m+[Model to use for embeddings]:MODEL:_lc_models' \
433 '--model=[Model to use for embeddings]:MODEL:_lc_models' \
434 '-p+[Provider to use for embeddings]:PROVIDER:_lc_providers' \
435 '--provider=[Provider to use for embeddings]:PROVIDER:_lc_providers' \
436 '-v+[Vector database name to search]:DATABASE:_lc_vectordbs' \
437 '--vectordb=[Vector database name to search]:DATABASE:_lc_vectordbs' \
438 '-l+[Number of similar results to return]:LIMIT:_default' \
439 '--limit=[Number of similar results to return]:LIMIT:_default' \
440 '-h[Print help]' \
441 '--help[Print help]' \
442 ':query -- Query text to find similar content:_default'
443 ret=$?
444 ;;
445 (v)
446 # Redirect 'v' alias to 'vectors' subcommand completion
447 words=("vectors" "${{words[@]:2}}")
448 (( CURRENT -= 1 ))
449 curcontext="${{curcontext%:*:*}}:lc-command-vectors:"
450 _lc__vectors_commands
451 ret=$?
452 ;;
453 (w)
454 # Redirect 'w' alias to 'web-chat-proxy' subcommand completion
455 words=("web-chat-proxy" "${{words[@]:2}}")
456 (( CURRENT -= 1 ))
457 curcontext="${{curcontext%:*:*}}:lc-command-web-chat-proxy:"
458 _lc__web_chat_proxy_commands
459 ret=$?
460 ;;
461 (sy)
462 # Redirect 'sy' alias to 'sync' subcommand completion
463 words=("sync" "${{words[@]:2}}")
464 (( CURRENT -= 1 ))
465 curcontext="${{curcontext%:*:*}}:lc-command-sync:"
466 _lc__sync_commands
467 ret=$?
468 ;;
469 (se)
470 # Redirect 'se' alias to 'search' subcommand completion
471 words=("search" "${{words[@]:2}}")
472 (( CURRENT -= 1 ))
473 curcontext="${{curcontext%:*:*}}:lc-command-search:"
474 _lc__search_commands
475 ret=$?
476 ;;
477 (img)
478 # Redirect 'img' alias to 'image' subcommand completion
479 words=("image" "${{words[@]:2}}")
480 (( CURRENT -= 1 ))
481 curcontext="${{curcontext%:*:*}}:lc-command-image:"
482 # Image command has no subcommands, so just complete its options
483 _arguments "${{_arguments_options[@]}}" : \
484 '-m+[Model to use for image generation]:MODEL:_lc_models' \
485 '--model=[Model to use for image generation]:MODEL:_lc_models' \
486 '-p+[Provider to use for image generation]:PROVIDER:_lc_providers' \
487 '--provider=[Provider to use for image generation]:PROVIDER:_lc_providers' \
488 '-s+[Image size]:SIZE:_default' \
489 '--size=[Image size]:SIZE:_default' \
490 '-n+[Number of images to generate]:COUNT:_default' \
491 '--count=[Number of images to generate]:COUNT:_default' \
492 '-o+[Output directory for generated images]:OUTPUT:_directories' \
493 '--output=[Output directory for generated images]:OUTPUT:_directories' \
494 '-d[Enable debug/verbose logging]' \
495 '--debug[Enable debug/verbose logging]' \
496 '-h[Print help]' \
497 '--help[Print help]' \
498 ':prompt -- Text prompt for image generation:_default'
499 ret=$?
500 ;;
501 (dump)
502 # Redirect 'dump' alias to 'dump-metadata' subcommand completion
503 words=("dump-metadata" "${{words[@]:2}}")
504 (( CURRENT -= 1 ))
505 curcontext="${{curcontext%:*:*}}:lc-command-dump-metadata:"
506 # Dump-metadata command has no subcommands, so just complete its options
507 _arguments "${{_arguments_options[@]}}" : \
508 '-l[List available cached metadata files]' \
509 '--list[List available cached metadata files]' \
510 '-h[Print help]' \
511 '--help[Print help]' \
512 '::provider -- Specific provider to dump:_lc_providers'
513 ret=$?
514 ;;
515 esac
516 fi
517
518 return ret
519}}
520
521# Replace the main completion function with our alias-aware version
522compdef _lc_with_aliases lc
523
524# Instructions for setup
525# Add the above to your ~/.zshrc or a file in your fpath to enable dynamic provider completion
526# Then run: source ~/.zshrc
527"#
528 );
529}
530
531fn generate_fish_dynamic_completions() {
533 println!(
534 r#"
535# Dynamic completion functions for lc (Fish)
536function __lc_complete_providers
537 lc providers list 2>/dev/null | grep " •" | awk '{{print $2}}' 2>/dev/null
538end
539
540# Add dynamic provider completion
541complete -c lc -s p -l provider -f -a "(__lc_complete_providers)" -d "Provider to use"
542
543# Instructions for setup
544# Add the above to ~/.config/fish/completions/lc.fish to enable dynamic provider completion
545# The file will be loaded automatically by Fish
546"#
547 );
548}
549
550#[allow(dead_code)]
552pub fn get_available_providers() -> Vec<String> {
553 match Config::load() {
554 Ok(config) => {
555 let mut providers: Vec<String> = config.providers.keys().cloned().collect();
556 providers.sort();
557 providers
558 }
559 Err(_) => Vec::new(),
560 }
561}
562
563#[allow(dead_code)]
565pub fn get_available_models() -> Vec<String> {
566 vec![
569 "gpt-4".to_string(),
570 "gpt-4-turbo".to_string(),
571 "gpt-3.5-turbo".to_string(),
572 "claude-3-sonnet".to_string(),
573 "claude-3-haiku".to_string(),
574 "gemini-pro".to_string(),
575 ]
576}
577
578#[allow(dead_code)]
580pub fn get_available_vectordbs() -> Vec<String> {
581 match crate::vector_db::VectorDatabase::list_databases() {
582 Ok(databases) => databases,
583 Err(_) => Vec::new(),
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590
591 #[test]
592 fn test_get_available_models() {
593 let models = get_available_models();
594 assert!(!models.is_empty());
595 assert!(models.contains(&"gpt-4".to_string()));
596 }
597}