From 4266f7cb003e7cea544d39623e1961fe66582e6f Mon Sep 17 00:00:00 2001 From: thestreamcode Date: Sat, 1 Aug 2026 22:24:36 +0200 Subject: [PATCH 1/3] feat: add --version flag and clearer Copilot launch failures Add `-v` / `--version` to print the installed switcher version, read from package.json so it stays correct for global and local installs. The flag is consumed by the switcher (like `--help`); `-- --version` still forwards to Copilot CLI. Replace the raw `spawn ... ENOENT` failure with an actionable message naming the resolved executable path, the install command, and the COPILOT_BIN override. Drop a dead `platform` argument passed to buildCopilotSpawnOptions, which does not accept it, and consolidate the duplicated src/cli.mjs import in the test suite. Add tests for --help, --version, interactive provider selection, interactive model selection, and the missing-binary path (39 -> 45 tests, line coverage 87.3% -> 93.0%). Co-Authored-By: Claude Opus 5 (1M context) --- src/args.mjs | 6 +++ src/cli.mjs | 26 +++++++-- test/args.test.mjs | 10 ++++ test/cli.test.mjs | 132 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/args.mjs b/src/args.mjs index c44b0ff..2429b73 100644 --- a/src/args.mjs +++ b/src/args.mjs @@ -9,6 +9,7 @@ export function parseArgs(argv) { wireApi: null, dryRun: false, help: false, + version: false, copilotArgs: [], }; @@ -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; diff --git a/src/cli.mjs b/src/cli.mjs index 42cfb9d..9f062a8 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -1,3 +1,4 @@ +import { readFile } from 'node:fs/promises'; import readline from 'node:readline/promises'; import spawn from 'cross-spawn'; @@ -19,6 +20,11 @@ export async function main(argv = process.argv.slice(2), io = defaultIo()) { return 0; } + if (args.version) { + io.stdout.write(`${await readPackageVersion()}\n`); + return 0; + } + if (args.providerName === 'native') { const copilotArgs = [...args.copilotArgs]; if (args.explicitModel) copilotArgs.unshift('--model', args.explicitModel); @@ -185,15 +191,29 @@ function runCopilot({ copilotArgs, env, io, dryRun, provider, wireModel, native return new Promise((resolve, reject) => { const child = spawn(copilotBin, copilotArgs, { - ...buildCopilotSpawnOptions({ env, ioEnv: io.env, platform: process.platform, config }), + ...buildCopilotSpawnOptions({ env, ioEnv: io.env, config }), stdio: 'inherit', }); - child.on('error', reject); + child.on('error', (error) => reject(describeSpawnError(error, copilotBin))); child.on('exit', (code) => resolve(code ?? 1)); }); } +function describeSpawnError(error, copilotBin) { + if (error?.code !== 'ENOENT') return error; + + return new Error( + `Could not launch GitHub Copilot CLI at "${copilotBin}". Install it with "npm install -g @github/copilot" or set COPILOT_BIN to the executable path.`, + { cause: error } + ); +} + +async function readPackageVersion() { + const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); + return manifest.version; +} + export function buildCopilotSpawnOptions({ env, ioEnv = process.env, config = null }) { return { env: sanitizeCopilotEnvironment(ioEnv, env, collectProviderSecretEnvNames(config)), @@ -228,5 +248,5 @@ function redactEnv(env) { } function helpText() { - return `copilot-byok - switch GitHub Copilot CLI between native and BYOK providers\n\nUsage:\n copilot-byok [options] [-- Copilot args...]\n\nOptions:\n -P, --provider Provider id or alias\n --native Run GitHub Copilot CLI without BYOK\n -m, --model Provider wire model for BYOK, native model for --native\n -c, --config Provider config JSON path\n --list-models Print ranked models for the selected provider\n --no-model-prompt Use automatic default model\n --offline Prevent Copilot from contacting GitHub in BYOK mode\n --wire-api BYOK wire API: completions or responses\n --dry-run Print command/env without launching Copilot\n -h, --help Show this help\n\nExamples:\n copilot-byok --provider chutes --no-model-prompt\n copilot-byok --provider openrouter --offline --no-model-prompt\n copilot-byok --provider alibaba-token-plan --wire-api responses --no-model-prompt\n copilot-byok --provider fireworks --model accounts/fireworks/models/minimax-m2p5 -p "fix the bug"\n copilot-byok --native --model claude-sonnet-4.6\n`; + return `copilot-byok - switch GitHub Copilot CLI between native and BYOK providers\n\nUsage:\n copilot-byok [options] [-- Copilot args...]\n\nOptions:\n -P, --provider Provider id or alias\n --native Run GitHub Copilot CLI without BYOK\n -m, --model Provider wire model for BYOK, native model for --native\n -c, --config Provider config JSON path\n --list-models Print ranked models for the selected provider\n --no-model-prompt Use automatic default model\n --offline Prevent Copilot from contacting GitHub in BYOK mode\n --wire-api BYOK wire API: completions or responses\n --dry-run Print command/env without launching Copilot\n -h, --help Show this help\n -v, --version Print the copilot-byok version\n\nExamples:\n copilot-byok --provider chutes --no-model-prompt\n copilot-byok --provider openrouter --offline --no-model-prompt\n copilot-byok --provider alibaba-token-plan --wire-api responses --no-model-prompt\n copilot-byok --provider fireworks --model accounts/fireworks/models/minimax-m2p5 -p "fix the bug"\n copilot-byok --native --model claude-sonnet-4.6\n`; } diff --git a/test/args.test.mjs b/test/args.test.mjs index 4988c74..77b27de 100644 --- a/test/args.test.mjs +++ b/test/args.test.mjs @@ -32,6 +32,16 @@ test('rejects empty option values and incompatible model listing options', () => assert.throws(() => parseArgs(['--native', '--offline']), /require a BYOK provider/); }); +test('parses help and version flags without forwarding them to Copilot', () => { + assert.equal(parseArgs(['-h']).help, true); + assert.equal(parseArgs(['--help']).help, true); + assert.equal(parseArgs(['-v']).version, true); + + const parsed = parseArgs(['--version']); + assert.equal(parsed.version, true); + assert.deepEqual(parsed.copilotArgs, []); +}); + test('parses offline mode and a Responses API override', () => { const parsed = parseArgs(['--provider', 'openrouter', '--offline', '--wire-api', 'RESPONSES']); diff --git a/test/cli.test.mjs b/test/cli.test.mjs index d862c62..92c57b8 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -2,13 +2,43 @@ import assert from 'node:assert/strict'; import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { Writable } from 'node:stream'; +import { Readable, Writable } from 'node:stream'; import test from 'node:test'; -import { main } from '../src/cli.mjs'; -import { buildCopilotSpawnOptions } from '../src/cli.mjs'; +import { buildCopilotSpawnOptions, main } from '../src/cli.mjs'; import { sanitizeCopilotEnvironment } from '../src/process-env.mjs'; +const packageVersion = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8') +).version; + +test('prints help without touching provider config', async () => { + const output = captureWritable(); + const exitCode = await main(['--help'], { + stdin: { isTTY: false }, + stdout: output, + stderr: captureWritable(), + env: { COPILOT_BYOK_CONFIG: join(tmpdir(), 'does-not-exist.json') }, + }); + + assert.equal(exitCode, 0); + assert.match(output.text(), /^copilot-byok - switch GitHub Copilot CLI/); + assert.match(output.text(), /-v, --version/); +}); + +test('prints the package version', async () => { + const output = captureWritable(); + const exitCode = await main(['--version'], { + stdin: { isTTY: false }, + stdout: output, + stderr: captureWritable(), + env: {}, + }); + + assert.equal(exitCode, 0); + assert.equal(output.text(), `${packageVersion}\n`); +}); + test('native mode does not require provider config to parse', async () => { const dir = await mkdtemp(join(tmpdir(), 'copilot-byok-')); const configPath = join(dir, 'broken.json'); @@ -340,7 +370,66 @@ test('times out model catalog requests', async () => { } }); -function captureWritable() { +test('selects a provider from the interactive menu', async () => { + const configPath = await writeProviderFixture(); + const output = captureWritable({ isTTY: true }); + + const exitCode = await main(['--config', configPath, '--no-model-prompt', '--dry-run'], { + stdin: readableTty(['2']), + stdout: output, + stderr: captureWritable(), + env: { SECOND_PROVIDER_KEY: 'secret' }, + }); + + const text = output.text(); + const result = JSON.parse(text.slice(text.indexOf('{\n'))); + assert.equal(exitCode, 0); + assert.match(text, /2\) Second Provider/); + assert.equal(result.provider, 'Second Provider'); + assert.equal(result.wireModel, 'second-model'); + assert.equal(result.env.COPILOT_PROVIDER_API_KEY, ''); +}); + +test('selects a ranked model from the interactive menu', async () => { + const configPath = await writeProviderFixture(); + const previousFetch = globalThis.fetch; + globalThis.fetch = async () => ({ + ok: true, + json: async () => ({ data: [{ id: 'model-a' }, { id: 'model-b' }] }), + }); + + const output = captureWritable({ isTTY: true }); + try { + const exitCode = await main(['--config', configPath, '--provider', 'first', '--dry-run'], { + stdin: readableTty(['2']), + stdout: output, + stderr: captureWritable(), + env: { FIRST_PROVIDER_KEY: 'secret' }, + }); + + const text = output.text(); + const result = JSON.parse(text.slice(text.indexOf('{\n'))); + assert.equal(exitCode, 0); + assert.match(text, /Available First Provider models/); + assert.equal(result.wireModel, 'model-b'); + } finally { + globalThis.fetch = previousFetch; + } +}); + +test('explains how to install Copilot CLI when the binary is missing', async () => { + await assert.rejects( + () => main(['--native'], { + stdin: { isTTY: false }, + stdout: captureWritable(), + stderr: captureWritable(), + env: { COPILOT_BIN: join(tmpdir(), 'copilot-byok-missing-binary') }, + }), + /Could not launch GitHub Copilot CLI/ + ); +}); + +function captureWritable({ isTTY = false } = {}) { let buffer = ''; const stream = new Writable({ write(chunk, encoding, callback) { @@ -348,6 +437,41 @@ function captureWritable() { callback(); }, }); + stream.isTTY = isTTY; stream.text = () => buffer; return stream; } + +function readableTty(lines) { + const stream = Readable.from(lines.map((line) => `${line}\n`), { objectMode: false }); + stream.isTTY = true; + return stream; +} + +async function writeProviderFixture() { + const dir = await mkdtemp(join(tmpdir(), 'copilot-byok-')); + const configPath = join(dir, 'providers.json'); + await writeFile(configPath, JSON.stringify({ + providers: [ + { + id: 'first', + name: 'First Provider', + type: 'openai', + baseUrl: 'https://api.first.example/v1', + modelsUrl: 'https://api.first.example/v1/models', + apiKeyEnv: 'FIRST_PROVIDER_KEY', + defaultModel: 'first-model', + }, + { + id: 'second', + name: 'Second Provider', + type: 'openai', + baseUrl: 'https://api.second.example/v1', + apiKeyEnv: 'SECOND_PROVIDER_KEY', + defaultModel: 'second-model', + }, + ], + })); + + return configPath; +} From bdd229d8311630d1717fe90e0a94d3c77fc7647c Mon Sep 17 00:00:00 2001 From: thestreamcode Date: Sat, 1 Aug 2026 22:24:48 +0200 Subject: [PATCH 2/3] chore: tidy lint scope, ignores, CI triggers and security contact - ESLint ignores `coverage/`, so linting after `npm run test:coverage` no longer depends on removing the report first. - `.gitignore` covers `*.tgz` (npm pack output) and `Thumbs.db`. - CI runs on pushes to `main` and `v*` tags, on pull requests, and on manual dispatch. Pull-request branches previously triggered two identical runs of the six-job matrix. Job names, and therefore the required status checks, are unchanged. - The issue-template security link points to SECURITY.md instead of a generic site URL. Co-Authored-By: Claude Opus 5 (1M context) --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- .github/workflows/ci.yml | 3 +++ .gitignore | 2 ++ eslint.config.js | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b105b89..710e95f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c30c81d..302aa41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,10 @@ name: CI on: push: + branches: [main] + tags: ['v*'] pull_request: + workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} diff --git a/.gitignore b/.gitignore index b90d628..72c4ad5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ npm-debug.log* .env.* !.env.example coverage/ +*.tgz .DS_Store +Thumbs.db diff --git a/eslint.config.js b/eslint.config.js index 0913809..189624a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,7 @@ import js from '@eslint/js'; import globals from 'globals'; export default [ + { ignores: ['coverage/'] }, js.configs.recommended, { files: ['**/*.mjs', '**/*.js'], From c20ca045504c0eef854401360e5c9dc513e4d2ac Mon Sep 17 00:00:00 2001 From: thestreamcode Date: Sat, 1 Aug 2026 22:24:58 +0200 Subject: [PATCH 3/3] docs: add AGENTS.md, expand README and release 0.2.0 - Add AGENTS.md with the project-specific stack, repository layout, real commands, code conventions, non-negotiable secret-handling rules, the checklist for adding a built-in provider, the validation gate, and the protected-branch release process. - README: document every CLI option (including --version), add the project structure, release process, contributing, security, changelog and license sections, and add Node engine and license badges. - Bump to 0.2.0 (backward-compatible feature) and keep package.json, package-lock.json, CITATION.cff, CHANGELOG.md and the pinned README versions in sync. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 32 +++++++++ CITATION.cff | 2 +- README.md | 69 +++++++++++++++++++- package-lock.json | 4 +- package.json | 2 +- 6 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..096dcd4 --- /dev/null +++ b/AGENTS.md @@ -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` 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 --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`), 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 688898c..90124a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CITATION.cff b/CITATION.cff index 43b3400..debf4e3 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -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 diff --git a/README.md b/README.md index b1e6750..6fc8278 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 ``` @@ -132,6 +135,24 @@ copilot-byok --provider openrouter --list-models copilot-byok --provider moonshot --list-models ``` +## Command-Line Options + +| Option | Description | +|---|---| +| `-P`, `--provider ` | Provider id or alias. | +| `--native` | Run GitHub Copilot CLI without BYOK. | +| `-m`, `--model ` | Provider wire model for BYOK, native model for `--native`. | +| `-c`, `--config ` | 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 ` | 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. @@ -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 +git push origin v +gh release create v --title "Copilot BYOK Switcher " --notes-file +``` + +`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. diff --git a/package-lock.json b/package-lock.json index 1a12fb4..e969228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "copilot-byok-switcher", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "copilot-byok-switcher", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" diff --git a/package.json b/package.json index 1ec4aca..8c0edb5 100644 --- a/package.json +++ b/package.json @@ -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",