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 +5 -3
- package/dist/main.js +531 -3
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|