Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Security report
url: https://mikesoft.it
about: Please report sensitive vulnerabilities privately.
url: https://github.com/TheStreamCode/copilot-byok-switcher/blob/main/SECURITY.md
about: Report vulnerabilities privately by following the security policy.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ name: CI

on:
push:
branches: [main]
tags: ['v*']
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ npm-debug.log*
.env.*
!.env.example
coverage/
*.tgz
.DS_Store
Thumbs.db
163 changes: 163 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# AGENTS.md

Operating instructions for AI agents and automation working on **copilot-byok-switcher**.
Read this file before changing anything in this repository.

## Project overview

`copilot-byok-switcher` is a published, public npm package (`copilot-byok-switcher`, MIT) that
installs a single cross-platform CLI, `copilot-byok`. It launches GitHub Copilot CLI either in
native mode or through a custom "bring your own key" (BYOK) model provider, by building the
`COPILOT_*` environment variables for the child Copilot process only. It never modifies the user's
shell profile and never writes credentials to disk.

Distribution channels actually configured:

- npm registry (public, `latest` dist-tag) — `npm publish` from the repository root.
- GitHub Releases with `v<version>` tags.

There is no VS Code extension, no bundler, no compiled output, and no hosted deployment.

## Stack and runtime

- Node.js **>= 22.13.0** (`engines.node`); CI runs Node 22 and 24 on Ubuntu, Windows, and macOS.
- Plain ESM JavaScript (`"type": "module"`, `.mjs` sources). **No TypeScript, no build step, no transpiler.**
- Package manager: **npm** with the committed `package-lock.json`. Never introduce pnpm, Yarn, or Bun,
and never add a second lockfile.
- Runtime dependencies: `cross-spawn` only. Dev dependencies: `eslint`, `@eslint/js`, `globals`.
Keep the runtime dependency surface minimal — prefer Node built-ins (`node:fs`, `node:readline/promises`,
global `fetch`, `AbortSignal.timeout`).
- Tests use the built-in `node:test` runner and `node:assert/strict`. No Jest, Vitest, or Mocha.

## Repository structure

```text
bin/copilot-byok.mjs Executable entry point; only wires main() to process.exitCode
src/args.mjs Pure argument parser; no I/O
src/cli.mjs Orchestration: prompts, model catalog fetch, Copilot spawn
src/config.mjs Built-in provider presets + provider config loading and validation
src/copilot-bin.mjs Resolves the Copilot executable, skipping the stale VS Code shim
src/model-ranking.mjs Pure ranking of provider model catalogs
src/process-env.mjs Strips stale/secret variables from the child environment
src/provider-env.mjs Builds the COPILOT_* variables for a selected provider
test/*.test.mjs One suite per src module
schemas/providers.schema.json Published JSON Schema for providers.json
examples/providers.example.json Documented example configuration
docs/provider-verification.md Evidence matrix for provider claims
```

## Commands

All commands run from the repository root.

| Purpose | Command |
|---|---|
| Install dependencies | `npm ci` |
| Lint | `npm run lint` |
| Test | `npm test` |
| Test with coverage | `npm run test:coverage` |
| Full quality gate | `npm run check` (lint + test) |
| Package contents check | `npm pack --dry-run` |
| Dependency audit | `npm audit --omit=dev --audit-level=high` |
| Local install for manual testing | `npm link` then `copilot-byok --help` |
| Publish (maintainer only) | `npm publish` (runs `prepack` → `npm run check`) |

There is no `dev`, `build`, `format`, or `type-check` script. Do not invent one in documentation.

## Conventions

- Two-space indentation, single quotes, semicolons, trailing commas in multiline literals.
- Named exports only; no default exports outside `bin/`.
- Modules stay small and single-purpose. `args.mjs`, `model-ranking.mjs`, `provider-env.mjs`, and
`process-env.mjs` must remain **pure** (no file, network, or process access) so they stay trivially testable.
- All I/O is injected through the `io` object (`{ stdin, stdout, stderr, env }`) passed to `main()`.
Never read `process.env` or write to `process.stdout` directly from `src/` outside the documented defaults.
- Errors are thrown as `Error` with actionable messages; `bin/copilot-byok.mjs` prints them prefixed with
`copilot-byok:` and sets a non-zero exit code.
- Conventional Commits for commit messages (`feat:`, `fix:`, `chore:`, `docs:`, `ci:`, `test:`).
- Files are LF-normalized through `.gitattributes`. Do not commit CRLF.

## Security rules (non-negotiable)

This project handles third-party provider API keys. Every change must preserve these guarantees:

- Credentials are read **only** from environment variables named by `apiKeyEnv` / `bearerTokenEnv`.
Inline `apiKey`, `bearerToken`, and secret-bearing `modelsHeaders` in provider config files are rejected
by `src/config.mjs` — keep those checks.
- Never print, log, or embed a credential value. `--dry-run` output must keep passing through `redactEnv()`,
which masks any key matching `/KEY|TOKEN|SECRET|PASSWORD/i`.
- Error messages must not reveal which environment variable holds a secret (covered by a test).
- Model-catalog requests send the bearer token only when the catalog URL is same-origin with `baseUrl`,
or when the provider explicitly sets `modelsAuth: true`. Do not weaken this default.
- `sanitizeCopilotEnvironment()` strips `COPILOT_PROVIDER_*` and every known provider source key
(case-insensitively) from the child environment. New built-in providers **must** have their key
environment names added to `DEFAULT_SECRET_SOURCE_ENV` in `src/process-env.mjs`.
- Processes are spawned with `shell: false` via `cross-spawn`. Never set `shell: true` and never build a
command string by concatenation — this is the Windows command-injection defense, and it is covered by a test.
- Catalog requests must keep a bounded timeout (`AbortSignal.timeout`, default 10 s, configurable 10–300000 ms).
- No `.env` file is used or expected; there is no `.env.example`. Do not add one.
- Never commit real keys, tokens, or a `providers.json` containing credentials.

## Adding or changing a built-in provider

1. Add the preset to `DEFAULT_PROVIDERS` in `src/config.mjs`, using only endpoints documented by the provider.
2. Add every credential environment name to `DEFAULT_SECRET_SOURCE_ENV` in `src/process-env.mjs`.
3. Mirror the entry in `examples/providers.example.json`.
4. Add the row to the provider table in `README.md` **with a link to the official API documentation**.
5. Update `docs/provider-verification.md` honestly: endpoint reachability, authenticated catalog access, and
end-to-end inference are three distinct evidence levels. Never claim a level that was not actually verified.
6. Extend `test/config.test.mjs` to cover the id, aliases, base URL, and credential environment names.

`catalogModelId` must be a model that exists in Copilot's built-in catalog (for `COPILOT_MODEL`);
the provider's own model name goes on the wire as `COPILOT_PROVIDER_WIRE_MODEL`. Do not merge the two.

## Compatibility and anti-breaking-change rules

- The CLI contract is public: existing flags, aliases, provider ids, and the `--dry-run` JSON shape must keep
working. Add options; do not rename or remove them.
- Unrecognized arguments and everything after `--` are forwarded verbatim to Copilot CLI. Do not start
consuming new argument names without documenting the change in `README.md` and `CHANGELOG.md`.
- `schemas/providers.schema.json` is referenced by `$id` from `main`; only widen it, never narrow it.
- Keep `engines.node` and the CI matrix in sync; raising the minimum Node version is a breaking change.

## Validation required before any commit

Run and pass all of these:

```sh
npm ci
npm run lint
npm test
npm pack --dry-run
npm audit --omit=dev --audit-level=high
```

Never disable a lint rule, skip a test, or bypass a hook to make the gate green. `npm pack --dry-run` must
show only `bin/`, `src/`, `examples/`, `docs/`, `schemas/`, `package.json`, `README.md`, `CHANGELOG.md`,
`SECURITY.md`, and `LICENSE` — no `node_modules`, no tests, no local configuration.

## Versioning and release

- Semantic Versioning. Patch for fixes, cleanup, and documentation; minor for backward-compatible features;
major only for intentional breaking changes.
- Bump with `npm version <new> --no-git-tag-version` so `package.json` and `package-lock.json` stay in sync,
then update `CITATION.cff`, `CHANGELOG.md`, and the pinned versions in `README.md` to the same number.
- `main` is protected: required status checks on all six CI jobs and **one approving review**. Push a branch
and open a pull request; never force-push, never rewrite history, never self-approve, never use admin bypass.
- Tag (`v<version>`), GitHub Release, and `npm publish` happen only after the pull request is merged into `main`
and CI is green.

## Generated or externally-owned files — do not hand-edit

- `package-lock.json` — regenerate through npm only.
- The version field in `package.json` — change it with `npm version`.
- Action SHAs in `.github/workflows/ci.yml` are pinned to immutable commits with a `# vX.Y.Z` comment.
Keep both in sync when updating.
- Dependabot version-update PRs were intentionally disabled for this repository. Do not re-enable them
without an explicit request.

## Repository visibility

This repository is **public** and the package is published to the public npm registry. Everything committed
here is world-readable: no internal URLs, no customer data, no credentials, no unverifiable claims, no
fabricated badges or statistics. Never change repository visibility.
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@ All notable changes to this project are documented in this file.

## [Unreleased]

## [0.2.0] - 2026-08-01

### Added

- `-v` / `--version` prints the installed `copilot-byok` version. The flag is consumed by the switcher and is
no longer forwarded to GitHub Copilot CLI; use `-- --version` to pass it through.
- A `Command-Line Options` table, a `Project Structure` section, and explicit `Release Process`,
`Contributing`, `Security`, `Changelog`, and `License` sections in `README.md`.
- `AGENTS.md` with the project-specific stack, commands, security rules, provider checklist, validation gate,
and release process for contributors and AI agents.
- Node.js engine and license badges in `README.md`.
- Tests for `--help`, `--version`, interactive provider selection, interactive model selection, and the
missing-binary error path (39 tests to 45; line coverage 87.3% to 93.0%).

### Changed

- A missing or unreachable Copilot executable now fails with an actionable message naming the resolved path
and the install command, instead of a raw `spawn ... ENOENT`.
- CI runs on pushes to `main` and to `v*` tags, on pull requests, and on manual dispatch, removing the
duplicate workflow run that every pull-request branch previously triggered.
- The security contact link in the issue-template chooser points to `SECURITY.md` instead of a generic site URL.

### Fixed

- Removed a dead `platform` argument passed to `buildCopilotSpawnOptions`, which does not accept it.

### Internal

- ESLint ignores `coverage/`, so `npm run lint` after `npm run test:coverage` no longer depends on cleanup.
- `.gitignore` covers `*.tgz` (`npm pack` output) and `Thumbs.db`.
- Consolidated the duplicated `src/cli.mjs` import in `test/cli.test.mjs`.

## [0.1.0] - 2026-08-01

- Published the first stable package to npm and created the matching GitHub
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ authors:
- family-names: Gasperini
given-names: Michael
url: "https://github.com/TheStreamCode/copilot-byok-switcher"
version: "0.1.0"
version: "0.2.0"
license: MIT
69 changes: 66 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[![npm version](https://img.shields.io/npm/v/copilot-byok-switcher)](https://www.npmjs.com/package/copilot-byok-switcher)
[![CI](https://github.com/TheStreamCode/copilot-byok-switcher/actions/workflows/ci.yml/badge.svg)](https://github.com/TheStreamCode/copilot-byok-switcher/actions/workflows/ci.yml)
[![node-current](https://img.shields.io/node/v/copilot-byok-switcher)](https://nodejs.org)
[![license](https://img.shields.io/npm/l/copilot-byok-switcher)](LICENSE)

Cross-platform launcher for GitHub Copilot CLI custom model providers (BYOK), with interactive selection and automatic model defaults.

Expand Down Expand Up @@ -42,21 +44,22 @@ Install the latest stable release from npm:
npm install -g copilot-byok-switcher
```

To pin the current release explicitly:
To pin an exact release explicitly:

```sh
npm install -g copilot-byok-switcher@0.1.0
npm install -g copilot-byok-switcher@0.2.0
```

The same version can be installed directly from its GitHub tag:

```sh
npm install -g github:TheStreamCode/copilot-byok-switcher#v0.1.0
npm install -g github:TheStreamCode/copilot-byok-switcher#v0.2.0
```

Then verify the CLI is available:

```sh
copilot-byok --version
copilot-byok --help
```

Expand Down Expand Up @@ -132,6 +135,24 @@ copilot-byok --provider openrouter --list-models
copilot-byok --provider moonshot --list-models
```

## Command-Line Options

| Option | Description |
|---|---|
| `-P`, `--provider <id>` | Provider id or alias. |
| `--native` | Run GitHub Copilot CLI without BYOK. |
| `-m`, `--model <model>` | Provider wire model for BYOK, native model for `--native`. |
| `-c`, `--config <path>` | Provider config JSON path. |
| `--list-models` | Print ranked models for the selected provider. |
| `--no-model-prompt` | Use the automatic default model. |
| `--offline` | Prevent Copilot from contacting GitHub in BYOK mode. |
| `--wire-api <api>` | BYOK wire API: `completions` or `responses`. |
| `--dry-run` | Print the resolved command and environment without launching Copilot. |
| `-h`, `--help` | Show the help text. |
| `-v`, `--version` | Print the `copilot-byok` version. |

Any other argument, and everything after `--`, is forwarded unchanged to GitHub Copilot CLI.

## Built-In Providers

The CLI includes defaults for Chutes, OpenCode Go, Fireworks AI, OpenRouter, Moonshot AI (Kimi), DeepSeek, Z.ai (GLM), MiniMax, Alibaba Model Studio Token Plan, and Tencent Cloud Token Plan.
Expand Down Expand Up @@ -332,10 +353,52 @@ copilot-byok --provider chutes --no-model-prompt --dry-run -p "hello"

See [Provider verification](docs/provider-verification.md) for the latest reproducible test matrix. It distinguishes endpoint reachability, authenticated catalog access, and complete Copilot CLI inference; these are intentionally not treated as equivalent claims.

## Project Structure

```text
bin/ Executable entry point (copilot-byok)
src/ CLI modules: argument parsing, config, model ranking, environment building
test/ node:test suites, one per src module
schemas/ JSON Schema for provider configuration files
examples/ Ready-to-copy provider configuration example
docs/ Provider verification matrix
```

## Release Process

Releases are cut from `main` after CI passes on every supported platform:

```sh
npm run check
npm pack --dry-run
npm publish
git tag v<version>
git push origin v<version>
gh release create v<version> --title "Copilot BYOK Switcher <version>" --notes-file <notes>
```

`package.json`, `CITATION.cff`, `CHANGELOG.md`, and the pinned versions in this README must all reference the same version before a release.

## Contributing

Issues and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) for the local quality gate and the requirements for provider changes, and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for expected conduct. Automation and AI agents should also read [AGENTS.md](AGENTS.md).

## Security

Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md). Do not open public issues for security reports and never include real API keys in issues, pull requests, or configuration examples.

## Changelog

Released changes are documented in [CHANGELOG.md](CHANGELOG.md).

## Support

If this CLI saves you time when testing Copilot BYOK providers, support continued maintenance through GitHub Sponsors: [github.com/sponsors/TheStreamCode](https://github.com/sponsors/TheStreamCode).

## License

[MIT](LICENSE) © Michael Gasperini (Mikesoft).

## Third-Party Notice

GitHub and GitHub Copilot are trademarks of GitHub, Inc. This project is not affiliated with or endorsed by GitHub.
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import js from '@eslint/js';
import globals from 'globals';

export default [
{ ignores: ['coverage/'] },
js.configs.recommended,
{
files: ['**/*.mjs', '**/*.js'],
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "copilot-byok-switcher",
"version": "0.1.0",
"version": "0.2.0",
"description": "Cross-platform launcher for GitHub Copilot CLI custom model providers (BYOK), with interactive selection and automatic model defaults.",
"keywords": [
"github-copilot",
Expand Down
6 changes: 6 additions & 0 deletions src/args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function parseArgs(argv) {
wireApi: null,
dryRun: false,
help: false,
version: false,
copilotArgs: [],
};

Expand All @@ -25,6 +26,11 @@ export function parseArgs(argv) {
continue;
}

if (arg === '--version' || arg === '-v') {
result.version = true;
continue;
}

if (arg === '--native') {
result.providerName = 'native';
continue;
Expand Down
Loading