copilot-relay 0.5.2 → 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
@@ -5401,12 +5401,14 @@ function normalizeResponsesToolSchema(schema) {
5401
5401
 
5402
5402
  //#endregion
5403
5403
  //#region src/copilot/responses.ts
5404
+ const minResponsesOutputTokens = 16;
5405
+ const responsesOutputTokens = (maxTokens) => typeof maxTokens === "number" && Number.isInteger(maxTokens) && maxTokens > 0 && maxTokens < minResponsesOutputTokens ? minResponsesOutputTokens : maxTokens;
5404
5406
  function buildResponsesRequestPayload(payload, reasoningEffort) {
5405
5407
  return {
5406
5408
  model: payload.model,
5407
5409
  input: translateMessagesToResponsesInput(payload.messages),
5408
5410
  stream: payload.stream,
5409
- max_output_tokens: payload.max_tokens,
5411
+ max_output_tokens: responsesOutputTokens(payload.max_tokens),
5410
5412
  temperature: payload.temperature,
5411
5413
  top_p: payload.top_p,
5412
5414
  user: sanitizeUserIdentifier(payload.user),
@@ -5552,7 +5554,7 @@ async function* translateResponsesStreamToChatCompletionStream(responseStream) {
5552
5554
  }
5553
5555
  if (event.type === "response.completed" || event.type === "response.incomplete") {
5554
5556
  if (!event.response) throw new Error("Upstream terminal event is missing its response.");
5555
- const completion = translateResponsesToChatCompletion({
5557
+ const completion$1 = translateResponsesToChatCompletion({
5556
5558
  ...event.response,
5557
5559
  status: event.type === "response.incomplete" ? "incomplete" : event.response.status ?? "completed",
5558
5560
  output: event.response.output ?? []
@@ -5581,7 +5583,7 @@ async function* translateResponsesStreamToChatCompletionStream(responseStream) {
5581
5583
  model: event.response.model
5582
5584
  }, {
5583
5585
  delta: {},
5584
- finishReason: completion.choices[0]?.finish_reason,
5586
+ finishReason: completion$1.choices[0]?.finish_reason,
5585
5587
  usage: translateUsage(event.response.usage)
5586
5588
  });
5587
5589
  completed = true;
@@ -10653,11 +10655,35 @@ const claudeAppDataDir = ({ platform, env, home } = currentPlatform()) => {
10653
10655
  return path.posix.join(configHome, "Claude-3p");
10654
10656
  };
10655
10657
  const standardDataDir = (dataDir) => dataDir.replace(/-3p$/, "");
10658
+ const otherVendorMarker = /ark-code|astron|command-r|deepseek|doubao|gemini|gemma|glm|gpt|grok|hermes|hy3|kimi|lfm|\bling\b|llama|longcat|mimo|minimax|mistral|mixtral|moonshot|nemotron|openai|phi-|qianfan|qwen|tc-code|\bunic\b|yi-|stepfun|step-3|seed-|bytedance|hunyuan|granite|amazon\.nova|nova-|devstral|ministral|ernie|codex|arcee|trinity|abab|phi\d|\bk2\.|\bm2\.|jamba|arctic|solar|mercury|zamba|kat-coder|\bds-|dpsk/;
10659
+ const looksAnthropic = (id) => {
10660
+ const lower = id.toLowerCase();
10661
+ return !otherVendorMarker.test(lower) && [
10662
+ "claude",
10663
+ "anthropic",
10664
+ "opus",
10665
+ "sonnet",
10666
+ "haiku",
10667
+ "fable",
10668
+ "mythos"
10669
+ ].some((word) => lower.includes(word));
10670
+ };
10671
+ const routesToTier = (name, tier) => name.toLowerCase().includes("opus") === (tier === "opus");
10672
+ /**
10673
+ * The name the app sends for a model: its own ID when the app accepts it and the relay routes that
10674
+ * ID to the model's own slot, otherwise a Claude-style route for its tier, such as
10675
+ * claude-sonnet-relay. Each tier therefore has its own name. The relay sends every model name without "opus"
10676
+ * to gptModel and every one with it to opusModel, so the route reaches the same upstream model. The
10677
+ * route names no real Claude model, so the app and its Claude Code take none of that model's
10678
+ * built-in context window or tuning; supports1m on the entry says what the window is.
10679
+ */
10680
+ const appModelName = (model) => looksAnthropic(model.id) && routesToTier(model.id, model.tier) ? model.id : `claude-${model.tier}-relay`;
10656
10681
  const inferenceModel = (model) => {
10657
10682
  const promptTokens = model.capabilities?.limits?.max_prompt_tokens ?? 0;
10658
- const label = model.capabilities?.name?.trim();
10683
+ const name = appModelName(model);
10684
+ const label = model.capabilities?.name?.trim() || (name === model.id ? void 0 : model.id);
10659
10685
  return {
10660
- name: model.id,
10686
+ name,
10661
10687
  ...label ? { labelOverride: label } : {},
10662
10688
  ...promptTokens >= 1e6 ? { supports1m: true } : {},
10663
10689
  anthropicFamilyTier: model.tier
@@ -10730,7 +10756,8 @@ const applyClaudeAppSetup = async (input) => {
10730
10756
  inferenceGatewayBaseUrl: input.baseUrl,
10731
10757
  inferenceGatewayApiKey: input.apiKey || existingKey || "copilot-relay",
10732
10758
  inferenceCredentialKind: "static",
10733
- inferenceModels: input.models.map(inferenceModel)
10759
+ inferenceModels: input.models.map(inferenceModel),
10760
+ modelDiscoveryEnabled: false
10734
10761
  };
10735
10762
  const desktopSnapshot = await readFileSnapshot(path.join(dataDir, "claude_desktop_config.json"));
10736
10763
  const desktop = {
@@ -11526,6 +11553,533 @@ const usage = defineCommand({
11526
11553
  }
11527
11554
  });
11528
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
+
11529
12083
  //#endregion
11530
12084
  //#region src/main.ts
11531
12085
  await runMain(defineCommand({
@@ -11543,7 +12097,8 @@ await runMain(defineCommand({
11543
12097
  start,
11544
12098
  status,
11545
12099
  stop,
11546
- usage
12100
+ usage,
12101
+ completion
11547
12102
  }
11548
12103
  }));
11549
12104