Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ Optional config at `~/.config/opencode/usage-config.jsonc`:
"openai": true,
"proxy": true,
"copilot": true
},

/**
* GitHub Copilot Enterprise/Organization Configuration
* For enterprise or organization-level usage tracking
*/
"copilotEnterprise": {
// Enterprise slug from GitHub Enterprise settings
// Use this for GitHub Enterprise Cloud accounts
"enterprise": "your-enterprise-slug",

// Organization name (alternative to enterprise)
// Use this for organization-level Copilot Business/Enterprise accounts
"organization": "your-org-name",

// Optional: override auth token
// Defaults to GitHub CLI token if not provided
// Token needs "View Enterprise Copilot Metrics" or "View Organization Copilot Metrics" permission
"token": ""
}
}
```
Expand All @@ -83,6 +102,26 @@ If missing, the plugin creates a default template on first run.

### Copilot auth

**Individual accounts** (Pro, Pro+, Free):
Detected from:
- `~/.local/share/opencode/copilot-usage-token.json`
- `~/.local/share/opencode/auth.json` with a `github-copilot` entry
- `~/.config/opencode/copilot-quota-token.json` (optional override)

**Enterprise/Organization accounts**:
Requires configuration in `usage-config.jsonc` (see above). The plugin will:
1. Check for `copilotEnterprise` config
2. Use enterprise/org metrics API if configured
3. Fall back to individual quota checking if enterprise metrics are unavailable
4. Automatically use GitHub CLI token if no explicit token is provided

**Enterprise Prerequisites**:
- "Copilot usage metrics" policy must be set to **Enabled everywhere** for the enterprise
- Token requires appropriate permissions:
- Fine-grained PAT: "Enterprise Copilot metrics" (read) or "Organization Copilot metrics" (read)
- Classic PAT: `manage_billing:copilot` or `read:enterprise` / `read:org`
- GitHub Enterprise Cloud account with Copilot Enterprise or Copilot Business

Copilot is detected from either of these locations:

- `~/.local/share/opencode/copilot-usage-token.json`
Expand Down
37 changes: 37 additions & 0 deletions src/providers/copilot/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import { existsSync } from "fs"
import { readFile } from "fs/promises"
import { join } from "path"
import { homedir } from "os"
import { getAuthFilePath } from "../../utils/paths.js"
import { type CopilotAuthData } from "./types.js"

Expand All @@ -25,3 +27,38 @@ export async function readCopilotAuth(): Promise<CopilotAuthData | null> {
return null
}
}

export async function readGitHubCliToken(): Promise<string | null> {
const ghConfigPath = join(homedir(), ".config", "gh", "hosts.yml")

try {
const file = Bun.file(ghConfigPath)
if (!(await file.exists())) {
return null
}

const content = await file.text()
const lines = content.split("\n")
let inGithubCom = false
for (const line of lines) {
const trimmed = line.trim()
if (trimmed === "github.com:" || trimmed === '"github.com":') {
inGithubCom = true
continue
}
if (inGithubCom && trimmed.startsWith("oauth_token:")) {
const match = trimmed.match(/oauth_token:\s*["']?([^"'\s]+)/)
if (match && match[1]) {
return match[1]
}
}
if (inGithubCom && trimmed.endsWith(":") && !trimmed.startsWith("oauth_token")) {
inGithubCom = false
}
}

return null
} catch {
return null
}
}
Comment on lines +31 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The readGitHubCliToken function manually parses the hosts.yml file by splitting it into lines and using a state machine. This approach is fragile and can easily break if the YAML file's format changes (e.g., different indentation, comments, or key ordering). A more robust solution would be to use a dedicated YAML parsing library (like js-yaml or yaml) to handle the file. This would make the code simpler and less prone to errors. For example: const config = yaml.parse(fileContent); const token = config?.['github.com']?.oauth_token;

Loading