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