copilot-relay 0.5.3 → 0.6.0

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.
package/README.md CHANGED
@@ -62,8 +62,8 @@ copilot-relay cache --hourly # or --daily; narrow with --since 6h or --m
62
62
  copilot-relay cache --json
63
63
  ```
64
64
 
65
- Check your Copilot plan and quota with `copilot-relay usage`; add `--json` for
66
- scripts. It asks GitHub with the stored token, so no relay needs to run.
65
+ Check your Copilot plan and quota with `copilot-relay usage`; add `--json` for scripts. It asks GitHub
66
+ with the stored token, so no relay needs to run. Tab completion: `eval "$(copilot-relay completion zsh)"` in `~/.zshrc`.
67
67
 
68
68
  ## Go further
69
69
 
@@ -71,7 +71,9 @@ scripts. It asks GitHub with the stored token, so no relay needs to run.
71
71
 
72
72
  - [Configure models and deep checks](wiki/EN-Configuration.md) · [中文](wiki/ZH-Configuration.md)
73
73
  - Run at login: [macOS](wiki/EN-macOS-LaunchAgent.md) · [Windows](wiki/EN-Windows-Service.md) · [Linux](wiki/EN-Linux-systemd.md)
74
- - [Commands](wiki/EN-Commands.md) · [Prompt caching](wiki/EN-Prompt-Caching.md) · [Troubleshoot](wiki/EN-Logging-Troubleshooting.md) · [Understand the architecture](wiki/EN-Architecture.md) · [Contribute](wiki/EN-Development.md)
74
+ - [Commands](wiki/EN-Commands.md) · [GitHub Copilot app](wiki/EN-Copilot-App.md) · [Prompt caching](wiki/EN-Prompt-Caching.md) · [Troubleshoot](wiki/EN-Logging-Troubleshooting.md) · [Understand the architecture](wiki/EN-Architecture.md) · [Contribute](wiki/EN-Development.md)
75
75
 
76
+ **Personal use only.** All forwarded usage runs on your own Copilot seat and limits; never let another person use your relay
77
+ ([why](wiki/EN-Configuration.md#personal-use-only) · [中文](wiki/ZH-Configuration.md#仅限个人使用)).
76
78
  Unofficial research project; not affiliated with GitHub, Anthropic or OpenAI. Upstream services and
77
79
  compatibility can change; model and tool support are not guaranteed (see the Wiki). [MIT licensed](LICENSE).
package/dist/main.js CHANGED
@@ -5554,7 +5554,7 @@ async function* translateResponsesStreamToChatCompletionStream(responseStream) {
5554
5554
  }
5555
5555
  if (event.type === "response.completed" || event.type === "response.incomplete") {
5556
5556
  if (!event.response) throw new Error("Upstream terminal event is missing its response.");
5557
- const completion = translateResponsesToChatCompletion({
5557
+ const completion$1 = translateResponsesToChatCompletion({
5558
5558
  ...event.response,
5559
5559
  status: event.type === "response.incomplete" ? "incomplete" : event.response.status ?? "completed",
5560
5560
  output: event.response.output ?? []
@@ -5583,7 +5583,7 @@ async function* translateResponsesStreamToChatCompletionStream(responseStream) {
5583
5583
  model: event.response.model
5584
5584
  }, {
5585
5585
  delta: {},
5586
- finishReason: completion.choices[0]?.finish_reason,
5586
+ finishReason: completion$1.choices[0]?.finish_reason,
5587
5587
  usage: translateUsage(event.response.usage)
5588
5588
  });
5589
5589
  completed = true;
@@ -11553,6 +11553,533 @@ const usage = defineCommand({
11553
11553
  }
11554
11554
  });
11555
11555
 
11556
+ //#endregion
11557
+ //#region src/lib/completion.ts
11558
+ const completionShells = [
11559
+ "zsh",
11560
+ "bash",
11561
+ "fish",
11562
+ "powershell"
11563
+ ];
11564
+ const isCompletionShell = (value) => completionShells.includes(value);
11565
+ const buildCompletionSpec = (commands, hints) => ({ commands: Object.entries(commands).map(([name, command]) => {
11566
+ const args = Object.entries(command.args ?? {});
11567
+ const options = args.filter(([, definition]) => definition.type !== "positional").map(([optionName, definition]) => {
11568
+ const values = hints[`${name} --${optionName}`];
11569
+ const option = {
11570
+ name: optionName,
11571
+ description: definition.description ?? "",
11572
+ takesValue: definition.type === "string"
11573
+ };
11574
+ if (values !== void 0) option.values = values;
11575
+ return option;
11576
+ });
11577
+ const hasPositional = args.some(([, definition]) => definition.type === "positional");
11578
+ return {
11579
+ name,
11580
+ description: command.meta?.description ?? "",
11581
+ options,
11582
+ positional: hasPositional ? hints[`${name} <>`] : void 0
11583
+ };
11584
+ }) });
11585
+ /**
11586
+ * gptModel and opusModel from config.yaml, for completing a model option. Reads the file without
11587
+ * creating or rewriting it, and gives no models rather than an error when it is missing or cannot
11588
+ * be read: a completion must never print a failure into the command line being typed.
11589
+ */
11590
+ const completionModels = async () => {
11591
+ try {
11592
+ const config = await readExistingAppConfig();
11593
+ if (config === void 0) return [];
11594
+ return [...new Set([config.gptModel, config.opusModel].filter((model) => model !== ""))];
11595
+ } catch {
11596
+ return [];
11597
+ }
11598
+ };
11599
+ const plainDescription = (text$1) => text$1.replace(/[$`\\]/g, "").replace(/\s+/g, " ").trim();
11600
+ const singleQuotePosix = (text$1) => `'${text$1.replaceAll("'", "'\\''")}'`;
11601
+ const singleQuoteFish = (text$1) => `'${text$1.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
11602
+ const singleQuotePowerShell = (text$1) => `'${text$1.replaceAll("'", "''")}'`;
11603
+ const valueOptions = (spec) => [...new Set(spec.commands.flatMap((command) => command.options.filter((option) => option.takesValue).map((option) => `--${option.name}`)))];
11604
+ const modelsCommand = "copilot-relay completion --models";
11605
+ const renderZsh = (spec) => {
11606
+ const zshDescription = (text$1) => plainDescription(text$1).replaceAll("[", "(").replaceAll("]", ")");
11607
+ const zshValues = (label, values) => {
11608
+ if (values === void 0) return `:${label}: `;
11609
+ if (values.kind === "words") return `:${label}:(${values.words.join(" ")})`;
11610
+ if (values.kind === "models") return `:${label}:_copilot_relay_models`;
11611
+ return `:${label}:_files -/`;
11612
+ };
11613
+ return `#compdef copilot-relay
11614
+ # zsh completion for copilot-relay. Generated by \`copilot-relay completion zsh\`.
11615
+
11616
+ _copilot_relay_models() {
11617
+ local -a models
11618
+ models=(\${(f)"$(${modelsCommand} 2>/dev/null)"})
11619
+ _describe -t models 'model' models
11620
+ }
11621
+
11622
+ _copilot_relay() {
11623
+ local -a commands
11624
+ commands=(
11625
+ ${spec.commands.map((command) => ` ${singleQuotePosix(`${command.name}:${plainDescription(command.description)}`)}`).join("\n")}
11626
+ )
11627
+
11628
+ if (( CURRENT == 2 )); then
11629
+ _describe -t commands 'copilot-relay command' commands
11630
+ _arguments '--help[Show help]' '--version[Show the version]'
11631
+ return
11632
+ fi
11633
+
11634
+ local command=\${words[2]}
11635
+ shift 1 words
11636
+ (( CURRENT-- ))
11637
+
11638
+ case $command in
11639
+ ${spec.commands.map((command) => {
11640
+ const specs = command.options.map((option) => {
11641
+ if (option.takesValue) return singleQuotePosix(`--${option.name}=[${zshDescription(option.description)}]${zshValues(option.name, option.values)}`);
11642
+ return singleQuotePosix(`--${option.name}[${zshDescription(option.description)}]`);
11643
+ });
11644
+ specs.push(singleQuotePosix("--help[Show help for this command]"));
11645
+ if (command.positional !== void 0) specs.push(singleQuotePosix(`*${zshValues("argument", command.positional)}`));
11646
+ return [
11647
+ ` (${command.name})`,
11648
+ ` _arguments -s ${specs.join(" \\\n ")}`,
11649
+ " ;;"
11650
+ ].join("\n");
11651
+ }).join("\n")}
11652
+ esac
11653
+ }
11654
+
11655
+ # Autoloaded from fpath, this file is the body of _copilot_relay; sourced or evaluated, it registers
11656
+ # the function with compinit's compdef.
11657
+ if [[ \${funcstack[1]} == _copilot_relay ]]; then
11658
+ _copilot_relay "$@"
11659
+ elif (( \${+functions[compdef]} )); then
11660
+ compdef _copilot_relay copilot-relay
11661
+ else
11662
+ print -u2 "copilot-relay completion: run compinit before loading this script"
11663
+ fi
11664
+ `;
11665
+ };
11666
+ const renderBash = (spec) => {
11667
+ const reply = (values, indent) => {
11668
+ if (values === void 0) return [`${indent}return 0`];
11669
+ if (values.kind === "words") return [`${indent}_copilot_relay_offer ${values.words.map(singleQuotePosix).join(" ")}`, `${indent}return 0`];
11670
+ if (values.kind === "models") return [`${indent}_copilot_relay_offer_quoted < <(_copilot_relay_models)`, `${indent}return 0`];
11671
+ return [`${indent}_copilot_relay_offer_directories`, `${indent}return 0`];
11672
+ };
11673
+ const commandCases = spec.commands.map((command) => {
11674
+ const lines = [` ${command.name})`];
11675
+ const valueOptionsOfCommand = command.options.filter((option) => option.takesValue);
11676
+ if (valueOptionsOfCommand.length > 0) {
11677
+ lines.push(" case $option in");
11678
+ for (const option of valueOptionsOfCommand) lines.push(` --${option.name})`, ...reply(option.values, " "), " ;;");
11679
+ lines.push(" esac");
11680
+ }
11681
+ if (command.positional !== void 0) lines.push(" if [[ $cur != -* ]]; then", ...reply(command.positional, " "), " fi");
11682
+ const options = [...command.options.map((option) => `--${option.name}`), "--help"];
11683
+ lines.push(` _copilot_relay_offer ${options.map(singleQuotePosix).join(" ")}`, " ;;");
11684
+ return lines.join("\n");
11685
+ });
11686
+ const topLevel = [
11687
+ ...spec.commands.map((command) => command.name),
11688
+ "--help",
11689
+ "--version"
11690
+ ];
11691
+ return `# bash completion for copilot-relay. Generated by \`copilot-relay completion bash\`.
11692
+
11693
+ _copilot_relay_models() {
11694
+ ${modelsCommand} 2>/dev/null
11695
+ }
11696
+
11697
+ # Offers each argument that starts with the word being completed. A case pattern compares the
11698
+ # quoted word as text, so neither the candidate nor the typed word is expanded.
11699
+ _copilot_relay_offer() {
11700
+ local candidate
11701
+ for candidate in "$@"; do
11702
+ case $candidate in
11703
+ "$cur"*)
11704
+ COMPREPLY+=("$candidate")
11705
+ ;;
11706
+ esac
11707
+ done
11708
+ }
11709
+
11710
+ # Offers each line of standard input that starts with the word being completed, escaped with
11711
+ # printf %q: bash inserts a completion as typed text, so an unescaped $(...) in a model name would
11712
+ # run when the line is entered.
11713
+ _copilot_relay_offer_quoted() {
11714
+ local candidate
11715
+ while IFS= read -r candidate; do
11716
+ case $candidate in
11717
+ "$cur"*)
11718
+ if [[ -n $candidate ]]; then
11719
+ COMPREPLY+=("$(printf '%q' "$candidate")")
11720
+ fi
11721
+ ;;
11722
+ esac
11723
+ done
11724
+ }
11725
+
11726
+ # Offers directories, escaped for the command line and ending in /, so a name with a space stays
11727
+ # one argument and the next Tab goes inside it.
11728
+ _copilot_relay_offer_directories() {
11729
+ local directory
11730
+ while IFS= read -r directory; do
11731
+ COMPREPLY+=("$(printf '%q' "$directory")/")
11732
+ done < <(compgen -d -- "$cur")
11733
+
11734
+ if type compopt >/dev/null 2>&1; then
11735
+ compopt -o nospace
11736
+ fi
11737
+ }
11738
+
11739
+ _copilot_relay() {
11740
+ local cur option command index word
11741
+ cur=\${COMP_WORDS[COMP_CWORD]}
11742
+ COMPREPLY=()
11743
+
11744
+ # The option whose value is being completed. bash 3.2 keeps "--since=6" as one word, and
11745
+ # readline replaces only the text after the =, so the value alone is completed. Later bash
11746
+ # splits it into "--since", "=" and "6", so the option is the word before the "=".
11747
+ option=\${COMP_WORDS[COMP_CWORD-1]}
11748
+ if [[ $cur == --*=* ]]; then
11749
+ option=\${cur%%=*}
11750
+ cur=\${cur#*=}
11751
+ elif [[ $option == = && COMP_CWORD -ge 2 ]]; then
11752
+ option=\${COMP_WORDS[COMP_CWORD-2]}
11753
+ fi
11754
+
11755
+ if [[ $cur == = ]]; then
11756
+ cur=
11757
+ fi
11758
+
11759
+ # The command is the first word that is neither an option nor an option's value.
11760
+ command=
11761
+ index=1
11762
+ while (( index < COMP_CWORD )); do
11763
+ word=\${COMP_WORDS[index]}
11764
+ case $word in
11765
+ ${valueOptions(spec).length === 0 ? "" : ` ${valueOptions(spec).join("|")})
11766
+ if [[ \${COMP_WORDS[index+1]} == = ]]; then
11767
+ index=$(( index + 3 ))
11768
+ else
11769
+ index=$(( index + 2 ))
11770
+ fi
11771
+
11772
+ continue
11773
+ ;;
11774
+ `} -*)
11775
+ ;;
11776
+ *)
11777
+ command=$word
11778
+ break
11779
+ ;;
11780
+ esac
11781
+
11782
+ index=$(( index + 1 ))
11783
+ done
11784
+
11785
+ if [[ -z $command ]]; then
11786
+ _copilot_relay_offer ${topLevel.map(singleQuotePosix).join(" ")}
11787
+ return 0
11788
+ fi
11789
+
11790
+ case $command in
11791
+ ${commandCases.join("\n")}
11792
+ esac
11793
+ }
11794
+
11795
+ complete -F _copilot_relay copilot-relay
11796
+ `;
11797
+ };
11798
+ const renderFish = (spec) => {
11799
+ const fishValues = (values) => {
11800
+ if (values === void 0) return "";
11801
+ if (values.kind === "words") return ` -a ${singleQuoteFish(values.words.join(" "))}`;
11802
+ if (values.kind === "models") return ` -a '(${modelsCommand} 2>/dev/null)'`;
11803
+ return " -a '(__fish_complete_directories)'";
11804
+ };
11805
+ const commandLines = spec.commands.map((command) => {
11806
+ const condition = `__copilot_relay_using ${command.name}`;
11807
+ const lines = [`complete -c copilot-relay -n '__copilot_relay_using' -a ${command.name} -d ${singleQuoteFish(plainDescription(command.description))}`];
11808
+ for (const option of command.options) {
11809
+ const value = option.takesValue ? ` -r${fishValues(option.values)}` : "";
11810
+ lines.push(`complete -c copilot-relay -n '${condition}' -l ${option.name}${value} -d ${singleQuoteFish(plainDescription(option.description))}`);
11811
+ }
11812
+ lines.push(`complete -c copilot-relay -n '${condition}' -l help -d 'Show help for this command'`);
11813
+ if (command.positional !== void 0) lines.push(`complete -c copilot-relay -n '${condition}'${fishValues(command.positional)}`);
11814
+ return lines.join("\n");
11815
+ });
11816
+ return `# fish completion for copilot-relay. Generated by \`copilot-relay completion fish\`.
11817
+
11818
+ # Prints the command word: the first word after copilot-relay that is neither an option nor the
11819
+ # value of an option that takes one.
11820
+ function __copilot_relay_command
11821
+ set -l words (commandline -opc)
11822
+ set -e words[1]
11823
+ set -l skip_next 0
11824
+ for word in $words
11825
+ if test $skip_next = 1
11826
+ set skip_next 0
11827
+ continue
11828
+ end
11829
+
11830
+ if string match -q -- '--*=*' $word
11831
+ continue
11832
+ end
11833
+
11834
+ if string match -q -- '--*' $word
11835
+ if contains -- (string sub -s 3 -- $word) ${valueOptions(spec).map((option) => option.slice(2)).join(" ")}
11836
+ set skip_next 1
11837
+ end
11838
+
11839
+ continue
11840
+ end
11841
+
11842
+ echo $word
11843
+ return 0
11844
+ end
11845
+
11846
+ return 1
11847
+ end
11848
+
11849
+ # True when the command word is the given one, or, with no argument, when there is none yet.
11850
+ function __copilot_relay_using
11851
+ set -l command (__copilot_relay_command)
11852
+ if test (count $argv) -eq 0
11853
+ test -z "$command"
11854
+ return
11855
+ end
11856
+
11857
+ test "$command" = $argv[1]
11858
+ end
11859
+
11860
+ complete -c copilot-relay -f
11861
+ complete -c copilot-relay -n '__copilot_relay_using' -l help -d 'Show help'
11862
+ complete -c copilot-relay -n '__copilot_relay_using' -l version -d 'Show the version'
11863
+
11864
+ ${commandLines.join("\n\n")}
11865
+ `;
11866
+ };
11867
+ const renderPowerShell = (spec) => {
11868
+ const valuesLiteral = (values) => {
11869
+ if (values === void 0) return "$null";
11870
+ if (values.kind === "words") return `@(${values.words.map(singleQuotePowerShell).join(", ")})`;
11871
+ return singleQuotePowerShell(values.kind);
11872
+ };
11873
+ return `# PowerShell completion for copilot-relay. Generated by \`copilot-relay completion powershell\`.
11874
+
11875
+ $script:CopilotRelayCommands = [ordered]@{
11876
+ ${spec.commands.map((command) => {
11877
+ const options = command.options.map((option) => ` ${singleQuotePowerShell(`--${option.name}`)} = @{ TakesValue = $${option.takesValue}; Values = ${valuesLiteral(option.values)}; Description = ${singleQuotePowerShell(plainDescription(option.description) || option.name)} }`);
11878
+ return [
11879
+ ` ${singleQuotePowerShell(command.name)} = @{`,
11880
+ ` Description = ${singleQuotePowerShell(plainDescription(command.description) || command.name)}`,
11881
+ ` Positional = ${valuesLiteral(command.positional)}`,
11882
+ " Options = [ordered]@{",
11883
+ ...options,
11884
+ " }",
11885
+ " }"
11886
+ ].join("\n");
11887
+ }).join("\n")}
11888
+ }
11889
+
11890
+ Register-ArgumentCompleter -Native -CommandName copilot-relay -ScriptBlock {
11891
+ param($wordToComplete, $commandAst, $cursorPosition)
11892
+
11893
+ # Text PowerShell would read differently is inserted in single quotes, so a model name or a path
11894
+ # with a space or a $( stays one literal argument.
11895
+ $quote = {
11896
+ param($text)
11897
+ if ($text -match "^[A-Za-z0-9._/\\\\:+-]+$") {
11898
+ return $text
11899
+ }
11900
+
11901
+ return "'" + ($text -replace "'", "''") + "'"
11902
+ }
11903
+
11904
+ # The words before the one being completed. An element that holds the cursor is the word being
11905
+ # completed, and an element after the cursor is not part of the question.
11906
+ $prior = @()
11907
+ $elements = @($commandAst.CommandElements | Select-Object -Skip 1)
11908
+ foreach ($element in $elements) {
11909
+ if ($element.Extent.EndOffset -ge $cursorPosition) {
11910
+ break
11911
+ }
11912
+
11913
+ $prior += $element.Extent.Text
11914
+ }
11915
+
11916
+ # The command is the first word that is neither an option nor an option's value.
11917
+ $command = $null
11918
+ $index = 0
11919
+ while ($index -lt $prior.Count) {
11920
+ $word = $prior[$index]
11921
+ if ($word.StartsWith('-')) {
11922
+ if ($null -ne $command -and -not $word.Contains('=')) {
11923
+ $known = $script:CopilotRelayCommands[$command].Options[$word]
11924
+ if ($null -ne $known -and $known.TakesValue) {
11925
+ $index += 1
11926
+ }
11927
+ }
11928
+ } elseif ($null -eq $command) {
11929
+ $command = $word
11930
+ }
11931
+
11932
+ $index += 1
11933
+ }
11934
+
11935
+ # "--since=2" completes the value after the =, and the inserted text keeps the option.
11936
+ $current = $wordToComplete
11937
+ $joined = ''
11938
+ $option = $null
11939
+ if ($null -ne $command -and $current -match '^(--[^=]+)=(.*)$') {
11940
+ $joined = $Matches[1] + '='
11941
+ $option = $script:CopilotRelayCommands[$command].Options[$Matches[1]]
11942
+ $current = $Matches[2]
11943
+ } elseif ($null -ne $command -and $prior.Count -gt 0) {
11944
+ $option = $script:CopilotRelayCommands[$command].Options[$prior[$prior.Count - 1]]
11945
+ }
11946
+
11947
+ $resolve = {
11948
+ param($values)
11949
+ if ($values -is [array]) {
11950
+ return $values | ForEach-Object { @{ Text = $_; Insert = $_; Tip = $_ } }
11951
+ }
11952
+
11953
+ if ($values -eq 'models') {
11954
+ return @(${modelsCommand} 2>$null) | Where-Object { $_ -ne '' } | ForEach-Object { @{ Text = $_; Insert = (& $quote $_); Tip = $_ } }
11955
+ }
11956
+
11957
+ if ($values -eq 'directories') {
11958
+ # The typed path up to its last separator stays as typed; directories are listed inside it.
11959
+ $cut = [Math]::Max($current.LastIndexOf('/'), $current.LastIndexOf('\\'))
11960
+ $prefix = ''
11961
+ $parent = '.'
11962
+ if ($cut -ge 0) {
11963
+ $prefix = $current.Substring(0, $cut + 1)
11964
+ $parent = $prefix
11965
+ }
11966
+
11967
+ return Get-ChildItem -LiteralPath $parent -Directory -ErrorAction SilentlyContinue | ForEach-Object {
11968
+ $path = $prefix + $_.Name + '/'
11969
+ @{ Text = $path; Insert = (& $quote $path); Tip = $_.FullName }
11970
+ }
11971
+ }
11972
+
11973
+ return @()
11974
+ }
11975
+
11976
+ $candidates = @()
11977
+ if ($null -eq $command -or -not $script:CopilotRelayCommands.Contains($command)) {
11978
+ foreach ($name in $script:CopilotRelayCommands.Keys) {
11979
+ $candidates += @{ Text = $name; Insert = $name; Tip = $script:CopilotRelayCommands[$name].Description }
11980
+ }
11981
+
11982
+ $candidates += @{ Text = '--help'; Insert = '--help'; Tip = 'Show help' }
11983
+ $candidates += @{ Text = '--version'; Insert = '--version'; Tip = 'Show the version' }
11984
+ } else {
11985
+ $entry = $script:CopilotRelayCommands[$command]
11986
+ if ($null -ne $option -and $option.TakesValue) {
11987
+ $candidates = @(& $resolve $option.Values)
11988
+ } elseif (-not $current.StartsWith('-') -and $null -ne $entry.Positional) {
11989
+ $candidates = @(& $resolve $entry.Positional)
11990
+ } else {
11991
+ foreach ($name in $entry.Options.Keys) {
11992
+ $candidates += @{ Text = $name; Insert = $name; Tip = $entry.Options[$name].Description }
11993
+ }
11994
+
11995
+ $candidates += @{ Text = '--help'; Insert = '--help'; Tip = 'Show help for this command' }
11996
+ }
11997
+ }
11998
+
11999
+ # A prefix comparison, not -like: a typed [ or * is text, not a wildcard.
12000
+ $candidates | Where-Object { $_.Text.StartsWith($current, [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object {
12001
+ $insert = $joined + $_.Insert
12002
+ [System.Management.Automation.CompletionResult]::new($insert, $_.Text, 'ParameterValue', $_.Tip)
12003
+ }
12004
+ }
12005
+ `;
12006
+ };
12007
+ const renderCompletion = (shell, spec) => {
12008
+ if (shell === "zsh") return renderZsh(spec);
12009
+ if (shell === "bash") return renderBash(spec);
12010
+ if (shell === "fish") return renderFish(spec);
12011
+ return renderPowerShell(spec);
12012
+ };
12013
+
12014
+ //#endregion
12015
+ //#region src/completion.ts
12016
+ const completionHints = {
12017
+ "setup <>": {
12018
+ kind: "words",
12019
+ words: [...setupTargets]
12020
+ },
12021
+ "completion <>": {
12022
+ kind: "words",
12023
+ words: [...completionShells]
12024
+ },
12025
+ "replay <>": { kind: "directories" },
12026
+ "models --model": { kind: "models" },
12027
+ "cache --model": { kind: "models" },
12028
+ "cache --since": {
12029
+ kind: "words",
12030
+ words: [
12031
+ "6h",
12032
+ "24h",
12033
+ "2d",
12034
+ "7d"
12035
+ ]
12036
+ }
12037
+ };
12038
+ const completion = defineCommand({
12039
+ meta: {
12040
+ name: "completion",
12041
+ description: `Print a shell completion script for ${completionShells.join(", ")}.`
12042
+ },
12043
+ args: {
12044
+ shell: {
12045
+ type: "positional",
12046
+ required: false,
12047
+ description: `Shell to complete: ${completionShells.join(", ")}.`
12048
+ },
12049
+ models: {
12050
+ type: "boolean",
12051
+ description: "Print gptModel and opusModel from config.yaml, one per line, for a completion script."
12052
+ }
12053
+ },
12054
+ async run({ args }) {
12055
+ if (args.models) {
12056
+ for (const model of await completionModels()) console.log(model);
12057
+ return;
12058
+ }
12059
+ const shell = typeof args.shell === "string" ? args.shell : "";
12060
+ if (!isCompletionShell(shell)) {
12061
+ console.error(`Name a shell: copilot-relay completion <${completionShells.join("|")}>.`);
12062
+ process.exitCode = 1;
12063
+ return;
12064
+ }
12065
+ process.stdout.write(renderCompletion(shell, await buildCompletionSpec$1()));
12066
+ }
12067
+ });
12068
+ const completionCommands = () => ({
12069
+ auth,
12070
+ cache,
12071
+ models,
12072
+ replay,
12073
+ restart,
12074
+ setup,
12075
+ start,
12076
+ status,
12077
+ stop,
12078
+ usage,
12079
+ completion
12080
+ });
12081
+ const buildCompletionSpec$1 = async () => buildCompletionSpec(completionCommands(), completionHints);
12082
+
11556
12083
  //#endregion
11557
12084
  //#region src/main.ts
11558
12085
  await runMain(defineCommand({
@@ -11570,7 +12097,8 @@ await runMain(defineCommand({
11570
12097
  start,
11571
12098
  status,
11572
12099
  stop,
11573
- usage
12100
+ usage,
12101
+ completion
11574
12102
  }
11575
12103
  }));
11576
12104