CLI: GitHub Copilot SDK integration
This commit is contained in:
parent
6859e1e6a6
commit
c98175fcc0
1
dist/control-ui/assets/index-08nzABV3.css
vendored
1
dist/control-ui/assets/index-08nzABV3.css
vendored
File diff suppressed because one or more lines are too long
3119
dist/control-ui/assets/index-DQcOTEYz.js
vendored
3119
dist/control-ui/assets/index-DQcOTEYz.js
vendored
File diff suppressed because one or more lines are too long
1
dist/control-ui/assets/index-DQcOTEYz.js.map
vendored
1
dist/control-ui/assets/index-DQcOTEYz.js.map
vendored
File diff suppressed because one or more lines are too long
15
docs/developer-guide.md
Normal file
15
docs/developer-guide.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Developer guide
|
||||
---
|
||||
|
||||
## Copilot SDK integration
|
||||
|
||||
When integrating GitHub Copilot, use the official `@github/copilot-sdk` package and avoid any other `@github/copilot*` packages. This keeps auditing simple and avoids divergent behavior.
|
||||
|
||||
Best practices:
|
||||
|
||||
- Centralize Copilot usage in a single module or provider.
|
||||
- Keep secrets and access tokens in config, not source code.
|
||||
- Add tests that cover Copilot entry points.
|
||||
|
||||
For enforcement details, see [Copilot SDK usage](/validation/copilot-usage).
|
||||
@ -10,16 +10,18 @@
|
||||
"colors": {
|
||||
"primary": "#FF5A36"
|
||||
},
|
||||
"topbarLinks": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/clawdbot/clawdbot"
|
||||
},
|
||||
{
|
||||
"name": "Releases",
|
||||
"url": "https://github.com/clawdbot/clawdbot/releases"
|
||||
}
|
||||
],
|
||||
"navbar": {
|
||||
"links": [
|
||||
{
|
||||
"label": "GitHub",
|
||||
"href": "https://github.com/clawdbot/clawdbot"
|
||||
},
|
||||
{
|
||||
"label": "Releases",
|
||||
"href": "https://github.com/clawdbot/clawdbot/releases"
|
||||
}
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/cron",
|
||||
@ -823,6 +825,13 @@
|
||||
"help/faq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Developer",
|
||||
"pages": [
|
||||
"developer-guide",
|
||||
"validation/copilot-usage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Install & Updates",
|
||||
"pages": [
|
||||
|
||||
91
docs/plans/plan.md
Normal file
91
docs/plans/plan.md
Normal file
@ -0,0 +1,91 @@
|
||||
# Copilot SDK Verification Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Verify and enforce the use of the official @github/copilot-sdk across the codebase.
|
||||
|
||||
**Architecture:** Scan the repository for Copilot integration points, audit dependencies, identify non-SDK usage, and implement an automated enforcement mechanism (lint or codemod). Validate via tests and CI, and update docs as needed.
|
||||
|
||||
**Tech Stack:** Node.js, TypeScript, ESLint/codemod, ripgrep, git, CI.
|
||||
|
||||
|
||||
### Task 1: Audit Copilot usage
|
||||
|
||||
**Files:**
|
||||
- Create: (none)
|
||||
- Modify: (none)
|
||||
- Test: (none)
|
||||
|
||||
**Step 1: Scan repository for Copilot references and SDK usage.**
|
||||
|
||||
```bash
|
||||
rg -n "@github/copilot-sdk|@github/copilot" -S
|
||||
```
|
||||
|
||||
Expected: list of files where Copilot is imported or configured. If none found, note absence and plan to add coverage.
|
||||
|
||||
**Step 2: Record findings.**
|
||||
|
||||
Document all occurrences in a summary file under `docs/validation/copilot-usage.md` including file paths and how Copilot is integrated.
|
||||
|
||||
### Task 2: Decide enforcement strategy
|
||||
|
||||
**Files:**
|
||||
- Create: `tools/copilot-enforcement/README.md`
|
||||
- Modify: `package.json` (optional) to add a lint script if desired
|
||||
- Test: `tests/copilot-usage.test.ts` or equivalent
|
||||
|
||||
**Option A (Recommended): ESLint rule to require @github/copilot-sdk**
|
||||
**Step 1:** Implement a custom ESLint rule that flags any import or require of Copilot outside the official SDK path, and auto-fix where possible.
|
||||
**Step 2:** Wire rule into ESLint config and run tests.
|
||||
**Step 3:** Add a test file asserting the rule flags non-SDK usage and allows SDK usage.
|
||||
|
||||
**Option B:** Codemod using jscodeshift to rewrite non-SDK usage to a safe pattern or emit a warning.
|
||||
|
||||
### Task 3: Add tests and run verification
|
||||
|
||||
**Files:**
|
||||
- Test: `tests/copilot-usage.test.ts` (for ESLint rule) or `tests/codemod/copilot-usage.js` (for codemod)
|
||||
|
||||
**Step 1:** Create test cases that cover:
|
||||
- Correct usage with @github/copilot-sdk
|
||||
- Incorrect usage with other Copilot integration patterns
|
||||
|
||||
**Step 2:** Run tests and ensure they pass/fail as expected.
|
||||
|
||||
**Step 3:** Integrate with CI so guards run on PRs.
|
||||
|
||||
### Task 4: Documentation and guidance
|
||||
|
||||
**Files:**
|
||||
- Update: `docs/developer-guide.md` with Copilot SDK usage best practices
|
||||
- Create: `docs/validation/copilot-usage.md` with rules and examples
|
||||
|
||||
### Task 5: Execute, verify, and commit
|
||||
|
||||
**Step 1:** Run full test suite locally.
|
||||
**Step 2:** If all tests pass, commit changes with a concise message.
|
||||
**Step 3:** Push and open a PR.
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
After saving the plan, offer execution choice:
|
||||
|
||||
**"Plan complete and saved to `docs/plans/<filename>.md`. Two execution options:**
|
||||
|
||||
**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
|
||||
|
||||
**Which approach?**
|
||||
|
||||
**If Subagent-Driven chosen:**
|
||||
- **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development
|
||||
- Stay in this session
|
||||
- Fresh subagent per task + code review
|
||||
|
||||
**If Parallel Session chosen:**
|
||||
- Guide them to open new session in worktree
|
||||
- **REQUIRED SUB-SKILL:** superpowers:executing-plans
|
||||
- Your operational mode has changed from plan to build.
|
||||
- You are no longer in read-only mode. You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
|
||||
@ -1,8 +1,8 @@
|
||||
---
|
||||
summary: "Sign in to GitHub Copilot from Clawdbot using the device flow"
|
||||
summary: "Use GitHub Copilot via the official Copilot CLI SDK"
|
||||
read_when:
|
||||
- You want to use GitHub Copilot as a model provider
|
||||
- You need the `clawdbot models auth login-github-copilot` flow
|
||||
- You need the Copilot CLI + SDK flow
|
||||
---
|
||||
# Github Copilot
|
||||
|
||||
@ -10,35 +10,25 @@ read_when:
|
||||
|
||||
GitHub Copilot is GitHub's AI coding assistant. It provides access to Copilot
|
||||
models for your GitHub account and plan. Clawdbot can use Copilot as a model
|
||||
provider in two different ways.
|
||||
provider via the official Copilot CLI SDK.
|
||||
|
||||
## Two ways to use Copilot in Clawdbot
|
||||
## Use Copilot in Clawdbot
|
||||
|
||||
### 1) Built-in GitHub Copilot provider (`github-copilot`)
|
||||
|
||||
Use the native device-login flow to obtain a GitHub token, then exchange it for
|
||||
Copilot API tokens when Clawdbot runs. This is the **default** and simplest path
|
||||
because it does not require VS Code.
|
||||
|
||||
### 2) Copilot Proxy plugin (`copilot-proxy`)
|
||||
|
||||
Use the **Copilot Proxy** VS Code extension as a local bridge. Clawdbot talks to
|
||||
the proxy’s `/v1` endpoint and uses the model list you configure there. Choose
|
||||
this when you already run Copilot Proxy in VS Code or need to route through it.
|
||||
You must enable the plugin and keep the VS Code extension running.
|
||||
|
||||
Use GitHub Copilot as a model provider (`github-copilot`). The login command runs
|
||||
the GitHub device flow, saves an auth profile, and updates your config to use that
|
||||
profile.
|
||||
Clawdbot integrates with GitHub Copilot through the official Copilot CLI SDK.
|
||||
This requires the Copilot CLI to be installed and authenticated on the gateway
|
||||
host.
|
||||
|
||||
## CLI setup
|
||||
|
||||
```bash
|
||||
clawdbot models auth login-github-copilot
|
||||
copilot auth login
|
||||
```
|
||||
|
||||
You'll be prompted to visit a URL and enter a one-time code. Keep the terminal
|
||||
open until it completes.
|
||||
Then validate Copilot in Clawdbot:
|
||||
|
||||
```bash
|
||||
clawdbot models auth login-github-copilot
|
||||
```
|
||||
|
||||
### Optional flags
|
||||
|
||||
@ -66,5 +56,4 @@ clawdbot models set github-copilot/gpt-4o
|
||||
- Requires an interactive TTY; run it directly in a terminal.
|
||||
- Copilot model availability depends on your plan; if a model is rejected, try
|
||||
another ID (for example `github-copilot/gpt-4.1`).
|
||||
- The login stores a GitHub token in the auth profile store and exchanges it for a
|
||||
Copilot API token when Clawdbot runs.
|
||||
- Copilot CLI must stay available on the gateway host.
|
||||
|
||||
38
docs/validation/copilot-usage.md
Normal file
38
docs/validation/copilot-usage.md
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Copilot SDK usage
|
||||
---
|
||||
|
||||
Clawdbot integrations that use GitHub Copilot must rely on the official `@github/copilot-sdk` package. This keeps the integration consistent and simplifies maintenance.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Use `@github/copilot-sdk` for all Copilot API integrations.
|
||||
- Do not import or depend on other `@github/copilot*` packages.
|
||||
- Keep Copilot usage in a dedicated module so usage can be audited easily.
|
||||
|
||||
## Automated enforcement
|
||||
|
||||
The repo ships a validation script that scans for non-SDK Copilot usage.
|
||||
|
||||
- Run `pnpm copilot:check` to validate the tree.
|
||||
- The check runs in CI via the Vitest guard in the test suite.
|
||||
|
||||
## Current status
|
||||
|
||||
Clawdbot integrates with GitHub Copilot via the official `@github/copilot-sdk` package.
|
||||
|
||||
## Examples
|
||||
|
||||
Allowed:
|
||||
|
||||
```
|
||||
import { createClient } from "@github/copilot-sdk";
|
||||
```
|
||||
|
||||
Disallowed:
|
||||
|
||||
```
|
||||
import { CopilotClient } from "@github/copilot";
|
||||
```
|
||||
|
||||
For guidance on integrating Copilot, see the [Developer guide](/developer-guide).
|
||||
@ -1,24 +1,3 @@
|
||||
# Copilot Proxy (Clawdbot plugin)
|
||||
# Copilot Proxy (removed)
|
||||
|
||||
Provider plugin for the **Copilot Proxy** VS Code extension.
|
||||
|
||||
## Enable
|
||||
|
||||
Bundled plugins are disabled by default. Enable this one:
|
||||
|
||||
```bash
|
||||
clawdbot plugins enable copilot-proxy
|
||||
```
|
||||
|
||||
Restart the Gateway after enabling.
|
||||
|
||||
## Authenticate
|
||||
|
||||
```bash
|
||||
clawdbot models auth login --provider copilot-proxy --set-default
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Copilot Proxy must be running in VS Code.
|
||||
- Base URL must include `/v1`.
|
||||
This plugin has been deprecated. Use the official Copilot SDK integration instead.
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
{
|
||||
"id": "copilot-proxy",
|
||||
"providers": [
|
||||
"copilot-proxy"
|
||||
],
|
||||
"providers": [],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -1,141 +1,9 @@
|
||||
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||
|
||||
const DEFAULT_BASE_URL = "http://localhost:3000/v1";
|
||||
const DEFAULT_API_KEY = "n/a";
|
||||
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
||||
const DEFAULT_MAX_TOKENS = 8192;
|
||||
const DEFAULT_MODEL_IDS = [
|
||||
"gpt-5.2",
|
||||
"gpt-5.2-codex",
|
||||
"gpt-5.1",
|
||||
"gpt-5.1-codex",
|
||||
"gpt-5.1-codex-max",
|
||||
"gpt-5-mini",
|
||||
"claude-opus-4.5",
|
||||
"claude-sonnet-4.5",
|
||||
"claude-haiku-4.5",
|
||||
"gemini-3-pro",
|
||||
"gemini-3-flash",
|
||||
"grok-code-fast-1",
|
||||
] as const;
|
||||
|
||||
function normalizeBaseUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return DEFAULT_BASE_URL;
|
||||
let normalized = trimmed;
|
||||
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
||||
if (!normalized.endsWith("/v1")) normalized = `${normalized}/v1`;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateBaseUrl(value: string): string | undefined {
|
||||
const normalized = normalizeBaseUrl(value);
|
||||
try {
|
||||
new URL(normalized);
|
||||
} catch {
|
||||
return "Enter a valid URL";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseModelIds(input: string): string[] {
|
||||
const parsed = input
|
||||
.split(/[\n,]/)
|
||||
.map((model) => model.trim())
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(parsed));
|
||||
}
|
||||
|
||||
function buildModelDefinition(modelId: string) {
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: "openai-completions",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
const copilotProxyPlugin = {
|
||||
id: "copilot-proxy",
|
||||
name: "Copilot Proxy",
|
||||
description: "Local Copilot Proxy (VS Code LM) provider plugin",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api) {
|
||||
api.registerProvider({
|
||||
id: "copilot-proxy",
|
||||
label: "Copilot Proxy",
|
||||
docsPath: "/providers/models",
|
||||
auth: [
|
||||
{
|
||||
id: "local",
|
||||
label: "Local proxy",
|
||||
hint: "Configure base URL + models for the Copilot Proxy server",
|
||||
kind: "custom",
|
||||
run: async (ctx) => {
|
||||
const baseUrlInput = await ctx.prompter.text({
|
||||
message: "Copilot Proxy base URL",
|
||||
initialValue: DEFAULT_BASE_URL,
|
||||
validate: validateBaseUrl,
|
||||
});
|
||||
|
||||
const modelInput = await ctx.prompter.text({
|
||||
message: "Model IDs (comma-separated)",
|
||||
initialValue: DEFAULT_MODEL_IDS.join(", "),
|
||||
validate: (value) =>
|
||||
parseModelIds(value).length > 0 ? undefined : "Enter at least one model id",
|
||||
});
|
||||
|
||||
const baseUrl = normalizeBaseUrl(baseUrlInput);
|
||||
const modelIds = parseModelIds(modelInput);
|
||||
const defaultModelId = modelIds[0] ?? DEFAULT_MODEL_IDS[0];
|
||||
const defaultModelRef = `copilot-proxy/${defaultModelId}`;
|
||||
|
||||
return {
|
||||
profiles: [
|
||||
{
|
||||
profileId: "copilot-proxy:local",
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "copilot-proxy",
|
||||
token: DEFAULT_API_KEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
configPatch: {
|
||||
models: {
|
||||
providers: {
|
||||
"copilot-proxy": {
|
||||
baseUrl,
|
||||
apiKey: DEFAULT_API_KEY,
|
||||
api: "openai-completions",
|
||||
authHeader: false,
|
||||
models: modelIds.map((modelId) => buildModelDefinition(modelId)),
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
models: Object.fromEntries(
|
||||
modelIds.map((modelId) => [`copilot-proxy/${modelId}`, {}]),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: defaultModelRef,
|
||||
notes: [
|
||||
"Start the Copilot Proxy VS Code extension before using these models.",
|
||||
"Copilot Proxy serves /v1/chat/completions; base URL must include /v1.",
|
||||
"Model availability depends on your Copilot plan; edit models.providers.copilot-proxy if needed.",
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
name: "Copilot Proxy (removed)",
|
||||
description: "Deprecated: use the official Copilot SDK integration instead.",
|
||||
register() {
|
||||
// Intentionally empty: copilot-proxy support has been removed.
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
"name": "@clawdbot/copilot-proxy",
|
||||
"version": "2026.1.25",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Copilot Proxy provider plugin",
|
||||
"description": "Deprecated: copilot-proxy integration removed in favor of the Copilot SDK.",
|
||||
"private": true,
|
||||
"deprecated": "Removed in favor of the official Copilot SDK integration.",
|
||||
"clawdbot": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
|
||||
@ -107,6 +107,7 @@
|
||||
"mac:restart": "bash scripts/restart-mac.sh",
|
||||
"mac:package": "bash scripts/package-mac-app.sh",
|
||||
"mac:open": "open dist/Clawdbot.app",
|
||||
"copilot:check": "node --import tsx tools/copilot-enforcement/check-copilot-usage.ts",
|
||||
"lint": "oxlint --type-aware src test",
|
||||
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
|
||||
"lint:all": "pnpm lint && pnpm lint:swift",
|
||||
@ -156,6 +157,7 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@github/copilot-sdk": "^0.1.18",
|
||||
"@homebridge/ciao": "^1.3.4",
|
||||
"@line/bot-sdk": "^10.6.0",
|
||||
"@lydell/node-pty": "1.2.0-beta.3",
|
||||
|
||||
173
pnpm-lock.yaml
generated
173
pnpm-lock.yaml
generated
@ -25,6 +25,9 @@ importers:
|
||||
'@clack/prompts':
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0
|
||||
'@github/copilot-sdk':
|
||||
specifier: ^0.1.18
|
||||
version: 0.1.18
|
||||
'@grammyjs/runner':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(grammy@1.39.3)
|
||||
@ -357,8 +360,8 @@ importers:
|
||||
extensions/memory-core:
|
||||
dependencies:
|
||||
clawdbot:
|
||||
specifier: '>=2026.1.25'
|
||||
version: link:../..
|
||||
specifier: '>=2026.1.24'
|
||||
version: 2026.1.24(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3)
|
||||
|
||||
extensions/memory-lancedb:
|
||||
dependencies:
|
||||
@ -982,6 +985,50 @@ packages:
|
||||
'@eshaz/web-worker@1.2.2':
|
||||
resolution: {integrity: sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==}
|
||||
|
||||
'@github/copilot-darwin-arm64@0.0.394':
|
||||
resolution: {integrity: sha512-qDmDFiFaYFW45UhxylN2JyQRLVGLCpkr5UmgbfH5e0aksf+69qytK/MwpD2Cq12KdTjyGMEorlADkSu5eftELA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
hasBin: true
|
||||
|
||||
'@github/copilot-darwin-x64@0.0.394':
|
||||
resolution: {integrity: sha512-iN4YwSVFxhASiBjLk46f+AzRTNHCvYcmyTKBASxieMIhnDxznYmpo+haFKPCv2lCsEWU8s5LARCnXxxx8J1wKA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
hasBin: true
|
||||
|
||||
'@github/copilot-linux-arm64@0.0.394':
|
||||
resolution: {integrity: sha512-9NeGvmO2tGztuneXZfYAyW3fDk6Pdl6Ffg8MAUaevA/p0awvA+ti/Vh0ZSTcI81nDTjkzONvrcIcjYAN7x0oSg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
hasBin: true
|
||||
|
||||
'@github/copilot-linux-x64@0.0.394':
|
||||
resolution: {integrity: sha512-toahsYQORrP/TPSBQ7sxj4/fJg3YUrD0ksCj/Z4y2vT6EwrE9iC2BspKgQRa4CBoCqxYDNB2blc+mQ1UuzPOxg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
hasBin: true
|
||||
|
||||
'@github/copilot-sdk@0.1.18':
|
||||
resolution: {integrity: sha512-BjHJihKOQXjd05mlG+aJ0fo/56q72CfbSHpTJvoSnQMJQ1oG2ztsYoQshFMCJYap/q0FtsYnIMu5x2v8nIzN8Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@github/copilot-win32-arm64@0.0.394':
|
||||
resolution: {integrity: sha512-R7XBP3l+oeDuBrP0KD80ZBEMsZoxAW8QO2MNsDUV8eVrNJnp6KtGHoA+iCsKYKNOD6wHA/q5qm/jR+gpsz46Aw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
hasBin: true
|
||||
|
||||
'@github/copilot-win32-x64@0.0.394':
|
||||
resolution: {integrity: sha512-/XYV8srP+pMXbf9Gc3wr58zCzBZvsdA3X4poSvr2uU8yCZ6E4pD0agFaZ1c/CikANJi8nb0Id3kulhEhePz/3A==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
hasBin: true
|
||||
|
||||
'@github/copilot@0.0.394':
|
||||
resolution: {integrity: sha512-koSiaHvVwjgppgh+puxf6dgsR8ql/WST1scS5bjzMsJFfWk7f4xtEXla7TCQfSGoZkCmCsr2Tis27v5TpssiCg==}
|
||||
hasBin: true
|
||||
|
||||
'@glideapps/ts-necessities@2.2.3':
|
||||
resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==}
|
||||
|
||||
@ -3124,6 +3171,11 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
clawdbot@2026.1.24:
|
||||
resolution: {integrity: sha512-foszbNXzk743kQBx2Nfc2KNlStZyBAVAYLQJ+KaONh009r8oB1a74kQ8wTnKX+SDNaQ9MvaE9tOisVUi+H3F+Q==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -5355,6 +5407,10 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vscode-jsonrpc@8.2.1:
|
||||
resolution: {integrity: sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -6496,6 +6552,39 @@ snapshots:
|
||||
'@eshaz/web-worker@1.2.2':
|
||||
optional: true
|
||||
|
||||
'@github/copilot-darwin-arm64@0.0.394':
|
||||
optional: true
|
||||
|
||||
'@github/copilot-darwin-x64@0.0.394':
|
||||
optional: true
|
||||
|
||||
'@github/copilot-linux-arm64@0.0.394':
|
||||
optional: true
|
||||
|
||||
'@github/copilot-linux-x64@0.0.394':
|
||||
optional: true
|
||||
|
||||
'@github/copilot-sdk@0.1.18':
|
||||
dependencies:
|
||||
'@github/copilot': 0.0.394
|
||||
vscode-jsonrpc: 8.2.1
|
||||
zod: 4.3.6
|
||||
|
||||
'@github/copilot-win32-arm64@0.0.394':
|
||||
optional: true
|
||||
|
||||
'@github/copilot-win32-x64@0.0.394':
|
||||
optional: true
|
||||
|
||||
'@github/copilot@0.0.394':
|
||||
optionalDependencies:
|
||||
'@github/copilot-darwin-arm64': 0.0.394
|
||||
'@github/copilot-darwin-x64': 0.0.394
|
||||
'@github/copilot-linux-arm64': 0.0.394
|
||||
'@github/copilot-linux-x64': 0.0.394
|
||||
'@github/copilot-win32-arm64': 0.0.394
|
||||
'@github/copilot-win32-x64': 0.0.394
|
||||
|
||||
'@glideapps/ts-necessities@2.2.3': {}
|
||||
|
||||
'@google/genai@1.34.0':
|
||||
@ -8862,6 +8951,84 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
clawdbot@2026.1.24(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock': 3.975.0
|
||||
'@buape/carbon': 0.14.0(hono@4.11.4)
|
||||
'@clack/prompts': 0.11.0
|
||||
'@grammyjs/runner': 2.0.3(grammy@1.39.3)
|
||||
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3)
|
||||
'@homebridge/ciao': 1.3.4
|
||||
'@line/bot-sdk': 10.6.0
|
||||
'@lydell/node-pty': 1.2.0-beta.3
|
||||
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||
'@mariozechner/pi-tui': 0.49.3
|
||||
'@mozilla/readability': 0.6.0
|
||||
'@sinclair/typebox': 0.34.47
|
||||
'@slack/bolt': 4.6.0(@types/express@5.0.6)
|
||||
'@slack/web-api': 7.13.0
|
||||
'@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
|
||||
ajv: 8.17.1
|
||||
body-parser: 2.2.2
|
||||
chalk: 5.6.2
|
||||
chokidar: 5.0.0
|
||||
chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482)
|
||||
cli-highlight: 2.1.11
|
||||
commander: 14.0.2
|
||||
croner: 9.1.0
|
||||
detect-libc: 2.1.2
|
||||
discord-api-types: 0.38.37
|
||||
dotenv: 17.2.3
|
||||
express: 5.2.1
|
||||
file-type: 21.3.0
|
||||
grammy: 1.39.3
|
||||
hono: 4.11.4
|
||||
jiti: 2.6.1
|
||||
json5: 2.2.3
|
||||
jszip: 3.10.1
|
||||
linkedom: 0.18.12
|
||||
long: 5.3.2
|
||||
markdown-it: 14.1.0
|
||||
node-edge-tts: 1.2.9
|
||||
osc-progress: 0.3.0
|
||||
pdfjs-dist: 5.4.530
|
||||
playwright-core: 1.58.0
|
||||
proper-lockfile: 4.1.2
|
||||
qrcode-terminal: 0.12.0
|
||||
sharp: 0.34.5
|
||||
sqlite-vec: 0.1.7-alpha.2
|
||||
tar: 7.5.4
|
||||
tslog: 4.10.2
|
||||
undici: 7.19.0
|
||||
ws: 8.19.0
|
||||
yaml: 2.8.2
|
||||
zod: 4.3.6
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.88
|
||||
node-llama-cpp: 3.15.0(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@discordjs/opus'
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- '@types/express'
|
||||
- audio-decode
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- canvas
|
||||
- debug
|
||||
- devtools-protocol
|
||||
- encoding
|
||||
- ffmpeg-static
|
||||
- jimp
|
||||
- link-preview-js
|
||||
- node-opus
|
||||
- opusscript
|
||||
- supports-color
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
cli-cursor@5.0.0:
|
||||
dependencies:
|
||||
restore-cursor: 5.1.0
|
||||
@ -11374,6 +11541,8 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vscode-jsonrpc@8.2.1: {}
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
171
src/agents/copilot-sdk-runner.ts
Normal file
171
src/agents/copilot-sdk-runner.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { CopilotClient } from "@github/copilot-sdk";
|
||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||
|
||||
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveSessionAgentIds } from "./agent-scope.js";
|
||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "./bootstrap-files.js";
|
||||
import { writeCliImages } from "./cli-runner/helpers.js";
|
||||
import { buildSystemPrompt } from "./cli-runner/helpers.js";
|
||||
import { resolveClawdbotDocsPath } from "./docs-path.js";
|
||||
import { FailoverError, resolveFailoverStatus } from "./failover-error.js";
|
||||
import { classifyFailoverReason, isFailoverErrorMessage } from "./pi-embedded-helpers.js";
|
||||
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||
|
||||
const log = createSubsystemLogger("agent/copilot-sdk");
|
||||
|
||||
export async function runCopilotSdkAgent(params: {
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config?: ClawdbotConfig;
|
||||
prompt: string;
|
||||
provider: string;
|
||||
model?: string;
|
||||
thinkLevel?: ThinkLevel;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
extraSystemPrompt?: string;
|
||||
ownerNumbers?: string[];
|
||||
cliSessionId?: string;
|
||||
images?: ImageContent[];
|
||||
}): Promise<EmbeddedPiRunResult> {
|
||||
const started = Date.now();
|
||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||
const workspaceDir = resolvedWorkspace;
|
||||
|
||||
const modelId = (params.model ?? "gpt-4.1").trim() || "gpt-4.1";
|
||||
const modelDisplay = `${params.provider}/${modelId}`;
|
||||
|
||||
const extraSystemPrompt = [
|
||||
params.extraSystemPrompt?.trim(),
|
||||
"Tools are disabled in this session. Do not call tools.",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
const sessionLabel = params.sessionKey ?? params.sessionId;
|
||||
const { contextFiles } = await resolveBootstrapContextForRun({
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
|
||||
});
|
||||
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
||||
sessionKey: params.sessionKey,
|
||||
config: params.config,
|
||||
});
|
||||
const heartbeatPrompt =
|
||||
sessionAgentId === defaultAgentId
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
: undefined;
|
||||
const docsPath = await resolveClawdbotDocsPath({
|
||||
workspaceDir,
|
||||
argv1: process.argv[1],
|
||||
cwd: process.cwd(),
|
||||
moduleUrl: import.meta.url,
|
||||
});
|
||||
const systemPrompt = buildSystemPrompt({
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
defaultThinkLevel: params.thinkLevel,
|
||||
extraSystemPrompt,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
heartbeatPrompt,
|
||||
docsPath: docsPath ?? undefined,
|
||||
tools: [],
|
||||
contextFiles,
|
||||
modelDisplay,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
|
||||
let cleanupImages: (() => Promise<void>) | undefined;
|
||||
let attachments: Array<{ type: "file"; path: string; displayName?: string }> | undefined;
|
||||
if (params.images && params.images.length > 0) {
|
||||
const imagePayload = await writeCliImages(params.images);
|
||||
cleanupImages = imagePayload.cleanup;
|
||||
attachments = imagePayload.paths.map((imagePath) => ({
|
||||
type: "file",
|
||||
path: imagePath,
|
||||
displayName: path.basename(imagePath),
|
||||
}));
|
||||
}
|
||||
|
||||
const client = new CopilotClient();
|
||||
|
||||
try {
|
||||
await client.start();
|
||||
|
||||
let session:
|
||||
| Awaited<ReturnType<CopilotClient["createSession"]>>
|
||||
| Awaited<ReturnType<CopilotClient["resumeSession"]>>;
|
||||
|
||||
if (params.cliSessionId) {
|
||||
try {
|
||||
session = await client.resumeSession(params.cliSessionId);
|
||||
} catch (err) {
|
||||
log.warn(`copilot-sdk resume failed, starting new session: ${String(err)}`);
|
||||
session = await client.createSession({
|
||||
model: modelId,
|
||||
systemMessage: { content: systemPrompt },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
session = await client.createSession({
|
||||
model: modelId,
|
||||
systemMessage: { content: systemPrompt },
|
||||
});
|
||||
}
|
||||
|
||||
const messageOptions: { prompt: string; attachments?: typeof attachments } = {
|
||||
prompt: params.prompt,
|
||||
};
|
||||
if (attachments && attachments.length > 0) {
|
||||
messageOptions.attachments = attachments;
|
||||
}
|
||||
|
||||
const response = await session.sendAndWait(messageOptions, params.timeoutMs);
|
||||
const text = response?.data?.content?.trim();
|
||||
if (!text) {
|
||||
throw new Error("Copilot SDK returned no response.");
|
||||
}
|
||||
|
||||
return {
|
||||
payloads: [{ text }],
|
||||
meta: {
|
||||
durationMs: Date.now() - started,
|
||||
agentMeta: {
|
||||
sessionId: session.sessionId,
|
||||
provider: params.provider,
|
||||
model: modelId,
|
||||
},
|
||||
},
|
||||
} satisfies EmbeddedPiRunResult;
|
||||
} catch (err) {
|
||||
if (err instanceof FailoverError) throw err;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (isFailoverErrorMessage(message)) {
|
||||
const reason = classifyFailoverReason(message) ?? "unknown";
|
||||
const status = resolveFailoverStatus(reason);
|
||||
throw new FailoverError(message, {
|
||||
reason,
|
||||
provider: params.provider,
|
||||
model: modelId,
|
||||
status,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (cleanupImages) {
|
||||
await cleanupImages();
|
||||
}
|
||||
await client.stop();
|
||||
}
|
||||
}
|
||||
@ -34,12 +34,17 @@ export function normalizeProviderId(provider: string): string {
|
||||
|
||||
export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
if (normalized === "github-copilot") return true;
|
||||
if (normalized === "claude-cli") return true;
|
||||
if (normalized === "codex-cli") return true;
|
||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
|
||||
}
|
||||
|
||||
export function isCopilotSdkProvider(provider: string): boolean {
|
||||
return normalizeProviderId(provider) === "github-copilot";
|
||||
}
|
||||
|
||||
function normalizeAnthropicModelId(model: string): string {
|
||||
const trimmed = model.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
|
||||
@ -427,7 +427,11 @@ export async function resolveImplicitCopilotProvider(params: {
|
||||
}): Promise<ProviderConfig | null> {
|
||||
const env = params.env ?? process.env;
|
||||
const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
|
||||
const hasProfile = listProfilesForProvider(authStore, "github-copilot").length > 0;
|
||||
const profileIds = listProfilesForProvider(authStore, "github-copilot");
|
||||
const hasProfile = profileIds.some((profileId) => {
|
||||
const profile = authStore.profiles[profileId];
|
||||
return profile?.type === "token" && Boolean(profile.token?.trim());
|
||||
});
|
||||
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
||||
const githubToken = (envToken ?? "").trim();
|
||||
|
||||
@ -437,7 +441,7 @@ export async function resolveImplicitCopilotProvider(params: {
|
||||
if (!selectedGithubToken && hasProfile) {
|
||||
// Use the first available profile as a default for discovery (it will be
|
||||
// re-resolved per-run by the embedded runner).
|
||||
const profileId = listProfilesForProvider(authStore, "github-copilot")[0];
|
||||
const profileId = profileIds[0];
|
||||
const profile = profileId ? authStore.profiles[profileId] : undefined;
|
||||
if (profile && profile.type === "token") {
|
||||
selectedGithubToken = profile.token;
|
||||
|
||||
@ -2,9 +2,10 @@ import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||
import { runCliAgent } from "../../agents/cli-runner.js";
|
||||
import { runCopilotSdkAgent } from "../../agents/copilot-sdk-runner.js";
|
||||
import { getCliSessionId } from "../../agents/cli-session.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
import { isCliProvider, isCopilotSdkProvider } from "../../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||
import {
|
||||
isCompactionFailureError,
|
||||
@ -150,6 +151,60 @@ export async function runAgentTurnWithFallback(params: {
|
||||
thinkLevel: params.followupRun.run.thinkLevel,
|
||||
});
|
||||
|
||||
if (isCopilotSdkProvider(provider)) {
|
||||
const startedAt = Date.now();
|
||||
emitAgentEvent({
|
||||
runId,
|
||||
stream: "lifecycle",
|
||||
data: {
|
||||
phase: "start",
|
||||
startedAt,
|
||||
},
|
||||
});
|
||||
const cliSessionId = getCliSessionId(params.getActiveSessionEntry(), provider);
|
||||
return runCopilotSdkAgent({
|
||||
sessionId: params.followupRun.run.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionFile: params.followupRun.run.sessionFile,
|
||||
workspaceDir: params.followupRun.run.workspaceDir,
|
||||
config: params.followupRun.run.config,
|
||||
prompt: params.commandBody,
|
||||
provider,
|
||||
model,
|
||||
thinkLevel: params.followupRun.run.thinkLevel,
|
||||
timeoutMs: params.followupRun.run.timeoutMs,
|
||||
runId,
|
||||
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
||||
ownerNumbers: params.followupRun.run.ownerNumbers,
|
||||
cliSessionId,
|
||||
images: params.opts?.images,
|
||||
})
|
||||
.then((result) => {
|
||||
emitAgentEvent({
|
||||
runId,
|
||||
stream: "lifecycle",
|
||||
data: {
|
||||
phase: "end",
|
||||
startedAt,
|
||||
endedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
emitAgentEvent({
|
||||
runId,
|
||||
stream: "lifecycle",
|
||||
data: {
|
||||
phase: "error",
|
||||
startedAt,
|
||||
endedAt: Date.now(),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
},
|
||||
});
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
if (isCliProvider(provider, params.followupRun.run.config)) {
|
||||
const startedAt = Date.now();
|
||||
emitAgentEvent({
|
||||
|
||||
@ -82,8 +82,8 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||
{
|
||||
value: "copilot",
|
||||
label: "Copilot",
|
||||
hint: "GitHub + local proxy",
|
||||
choices: ["github-copilot", "copilot-proxy"],
|
||||
hint: "GitHub Copilot CLI",
|
||||
choices: ["github-copilot"],
|
||||
},
|
||||
{
|
||||
value: "openrouter",
|
||||
@ -204,8 +204,8 @@ export function buildAuthChoiceOptions(params: {
|
||||
});
|
||||
options.push({
|
||||
value: "github-copilot",
|
||||
label: "GitHub Copilot (GitHub device login)",
|
||||
hint: "Uses GitHub device flow",
|
||||
label: "GitHub Copilot (Copilot CLI)",
|
||||
hint: "Uses the official Copilot CLI + SDK",
|
||||
});
|
||||
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
||||
options.push({
|
||||
@ -220,11 +220,6 @@ export function buildAuthChoiceOptions(params: {
|
||||
});
|
||||
options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
|
||||
options.push({ value: "qwen-portal", label: "Qwen OAuth" });
|
||||
options.push({
|
||||
value: "copilot-proxy",
|
||||
label: "Copilot Proxy (local)",
|
||||
hint: "Local proxy for VS Code Copilot models",
|
||||
});
|
||||
options.push({ value: "apiKey", label: "Anthropic API key" });
|
||||
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
||||
options.push({
|
||||
|
||||
@ -1,14 +1,2 @@
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
||||
|
||||
export async function applyAuthChoiceCopilotProxy(
|
||||
params: ApplyAuthChoiceParams,
|
||||
): Promise<ApplyAuthChoiceResult | null> {
|
||||
return await applyAuthChoicePluginProvider(params, {
|
||||
authChoice: "copilot-proxy",
|
||||
pluginId: "copilot-proxy",
|
||||
providerId: "copilot-proxy",
|
||||
methodId: "local",
|
||||
label: "Copilot Proxy",
|
||||
});
|
||||
}
|
||||
// Deprecated: copilot-proxy integration removed in favor of the official Copilot SDK.
|
||||
export {};
|
||||
|
||||
@ -11,7 +11,7 @@ export async function applyAuthChoiceGitHubCopilot(
|
||||
|
||||
await params.prompter.note(
|
||||
[
|
||||
"This will open a GitHub device login to authorize Copilot.",
|
||||
"This will open Copilot CLI login to authorize GitHub Copilot.",
|
||||
"Requires an active GitHub Copilot subscription.",
|
||||
].join("\n"),
|
||||
"GitHub Copilot",
|
||||
|
||||
@ -3,7 +3,6 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
|
||||
import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js";
|
||||
import { applyAuthChoiceCopilotProxy } from "./auth-choice.apply.copilot-proxy.js";
|
||||
import { applyAuthChoiceGitHubCopilot } from "./auth-choice.apply.github-copilot.js";
|
||||
import { applyAuthChoiceGoogleAntigravity } from "./auth-choice.apply.google-antigravity.js";
|
||||
import { applyAuthChoiceGoogleGeminiCli } from "./auth-choice.apply.google-gemini-cli.js";
|
||||
@ -44,7 +43,6 @@ export async function applyAuthChoice(
|
||||
applyAuthChoiceGitHubCopilot,
|
||||
applyAuthChoiceGoogleAntigravity,
|
||||
applyAuthChoiceGoogleGeminiCli,
|
||||
applyAuthChoiceCopilotProxy,
|
||||
applyAuthChoiceQwenPortal,
|
||||
];
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"synthetic-api-key": "synthetic",
|
||||
"venice-api-key": "venice",
|
||||
"github-copilot": "github-copilot",
|
||||
"copilot-proxy": "copilot-proxy",
|
||||
"minimax-cloud": "minimax",
|
||||
"minimax-api": "minimax",
|
||||
"minimax-api-lightning": "minimax",
|
||||
|
||||
@ -29,7 +29,6 @@ export type AuthChoice =
|
||||
| "minimax-api-lightning"
|
||||
| "opencode-zen"
|
||||
| "github-copilot"
|
||||
| "copilot-proxy"
|
||||
| "qwen-portal"
|
||||
| "skip";
|
||||
export type GatewayAuthChoice = "off" | "token" | "password";
|
||||
|
||||
@ -32,7 +32,6 @@ const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [
|
||||
{ pluginId: "google-antigravity-auth", providerId: "google-antigravity" },
|
||||
{ pluginId: "google-gemini-cli-auth", providerId: "google-gemini-cli" },
|
||||
{ pluginId: "qwen-portal-auth", providerId: "qwen-portal" },
|
||||
{ pluginId: "copilot-proxy", providerId: "copilot-proxy" },
|
||||
];
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
@ -328,11 +328,14 @@ describe("runHeartbeatOnce", () => {
|
||||
it("uses the last non-empty payload for delivery", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const workspaceDir = path.join(tmpDir, "workspace");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
try {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
heartbeat: { every: "5m", target: "whatsapp" },
|
||||
},
|
||||
},
|
||||
@ -455,12 +458,15 @@ describe("runHeartbeatOnce", () => {
|
||||
it("runs heartbeats in the explicit session key when configured", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const workspaceDir = path.join(tmpDir, "workspace");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
try {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const groupId = "120363401234567890@g.us";
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
heartbeat: {
|
||||
every: "5m",
|
||||
target: "last",
|
||||
@ -592,11 +598,14 @@ describe("runHeartbeatOnce", () => {
|
||||
it("can include reasoning payloads when enabled", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const workspaceDir = path.join(tmpDir, "workspace");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
try {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
heartbeat: {
|
||||
every: "5m",
|
||||
target: "whatsapp",
|
||||
@ -663,11 +672,14 @@ describe("runHeartbeatOnce", () => {
|
||||
it("delivers reasoning even when the main heartbeat reply is HEARTBEAT_OK", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const workspaceDir = path.join(tmpDir, "workspace");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
try {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
heartbeat: {
|
||||
every: "5m",
|
||||
target: "whatsapp",
|
||||
@ -733,11 +745,13 @@ describe("runHeartbeatOnce", () => {
|
||||
it("loads the default agent session from templated stores", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||
const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions.json");
|
||||
const workspaceDir = path.join(tmpDir, "workspace");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
try {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: {
|
||||
defaults: { heartbeat: { every: "5m" } },
|
||||
defaults: { workspace: workspaceDir, heartbeat: { every: "5m" } },
|
||||
list: [{ id: "work", default: true }],
|
||||
},
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
|
||||
@ -1,119 +1,15 @@
|
||||
import { CopilotClient } from "@github/copilot-sdk";
|
||||
import { intro, note, outro, spinner } from "@clack/prompts";
|
||||
|
||||
import { ensureAuthProfileStore, upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||
import { updateConfig } from "../commands/models/shared.js";
|
||||
import { applyAuthProfileConfig } from "../commands/onboard-auth.js";
|
||||
import { logConfigUpdated } from "../config/logging.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
|
||||
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
||||
const DEVICE_CODE_URL = "https://github.com/login/device/code";
|
||||
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
||||
|
||||
type DeviceCodeResponse = {
|
||||
device_code: string;
|
||||
user_code: string;
|
||||
verification_uri: string;
|
||||
expires_in: number;
|
||||
interval: number;
|
||||
};
|
||||
|
||||
type DeviceTokenResponse =
|
||||
| {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
scope?: string;
|
||||
}
|
||||
| {
|
||||
error: string;
|
||||
error_description?: string;
|
||||
error_uri?: string;
|
||||
};
|
||||
|
||||
function parseJsonResponse<T>(value: unknown): T {
|
||||
if (!value || typeof value !== "object") {
|
||||
throw new Error("Unexpected response from GitHub");
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
async function requestDeviceCode(params: { scope: string }): Promise<DeviceCodeResponse> {
|
||||
const body = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
scope: params.scope,
|
||||
});
|
||||
|
||||
const res = await fetch(DEVICE_CODE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`GitHub device code failed: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = parseJsonResponse<DeviceCodeResponse>(await res.json());
|
||||
if (!json.device_code || !json.user_code || !json.verification_uri) {
|
||||
throw new Error("GitHub device code response missing fields");
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
async function pollForAccessToken(params: {
|
||||
deviceCode: string;
|
||||
intervalMs: number;
|
||||
expiresAt: number;
|
||||
}): Promise<string> {
|
||||
const bodyBase = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
device_code: params.deviceCode,
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
});
|
||||
|
||||
while (Date.now() < params.expiresAt) {
|
||||
const res = await fetch(ACCESS_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: bodyBase,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`GitHub device token failed: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const json = parseJsonResponse<DeviceTokenResponse>(await res.json());
|
||||
if ("access_token" in json && typeof json.access_token === "string") {
|
||||
return json.access_token;
|
||||
}
|
||||
|
||||
const err = "error" in json ? json.error : "unknown";
|
||||
if (err === "authorization_pending") {
|
||||
await new Promise((r) => setTimeout(r, params.intervalMs));
|
||||
continue;
|
||||
}
|
||||
if (err === "slow_down") {
|
||||
await new Promise((r) => setTimeout(r, params.intervalMs + 2000));
|
||||
continue;
|
||||
}
|
||||
if (err === "expired_token") {
|
||||
throw new Error("GitHub device code expired; run login again");
|
||||
}
|
||||
if (err === "access_denied") {
|
||||
throw new Error("GitHub login cancelled");
|
||||
}
|
||||
throw new Error(`GitHub device flow error: ${err}`);
|
||||
}
|
||||
|
||||
throw new Error("GitHub device code expired; run login again");
|
||||
}
|
||||
const COPILOT_LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
|
||||
export async function githubCopilotLoginCommand(
|
||||
opts: { profileId?: string; yes?: boolean },
|
||||
@ -137,36 +33,36 @@ export async function githubCopilotLoginCommand(
|
||||
);
|
||||
}
|
||||
|
||||
const spin = spinner();
|
||||
spin.start("Requesting device code from GitHub...");
|
||||
const device = await requestDeviceCode({ scope: "read:user" });
|
||||
spin.stop("Device code ready");
|
||||
|
||||
note(
|
||||
[`Visit: ${device.verification_uri}`, `Code: ${device.user_code}`].join("\n"),
|
||||
stylePromptTitle("Authorize"),
|
||||
);
|
||||
|
||||
const expiresAt = Date.now() + device.expires_in * 1000;
|
||||
const intervalMs = Math.max(1000, device.interval * 1000);
|
||||
|
||||
const polling = spinner();
|
||||
polling.start("Waiting for GitHub authorization...");
|
||||
const accessToken = await pollForAccessToken({
|
||||
deviceCode: device.device_code,
|
||||
intervalMs,
|
||||
expiresAt,
|
||||
const loginSpin = spinner();
|
||||
loginSpin.start("Starting Copilot CLI login...");
|
||||
const loginResult = await runCommandWithTimeout(["copilot", "auth", "login"], {
|
||||
timeoutMs: COPILOT_LOGIN_TIMEOUT_MS,
|
||||
});
|
||||
polling.stop("GitHub access token acquired");
|
||||
loginSpin.stop("Copilot CLI login complete");
|
||||
|
||||
if (loginResult.code !== 0) {
|
||||
const message =
|
||||
loginResult.stderr.trim() || loginResult.stdout.trim() || "Copilot login failed";
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const clientSpin = spinner();
|
||||
clientSpin.start("Validating Copilot SDK connection...");
|
||||
const client = new CopilotClient();
|
||||
try {
|
||||
await client.start();
|
||||
await client.ping("clawdbot");
|
||||
} finally {
|
||||
await client.stop();
|
||||
}
|
||||
clientSpin.stop("Copilot SDK ready");
|
||||
|
||||
upsertAuthProfile({
|
||||
profileId,
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
token: accessToken,
|
||||
// GitHub device flow token doesn't reliably include expiry here.
|
||||
// Leave expires unset; we'll exchange into Copilot token plus expiry later.
|
||||
token: "",
|
||||
},
|
||||
});
|
||||
|
||||
@ -179,7 +75,7 @@ export async function githubCopilotLoginCommand(
|
||||
);
|
||||
|
||||
logConfigUpdated(runtime);
|
||||
runtime.log(`Auth profile: ${profileId} (github-copilot/token)`);
|
||||
runtime.log(`Auth profile: ${profileId} (github-copilot/cli)`);
|
||||
|
||||
outro("Done");
|
||||
}
|
||||
|
||||
13
test/copilot-usage.test.ts
Normal file
13
test/copilot-usage.test.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { scanCopilotUsage } from "../tools/copilot-enforcement/check-copilot-usage";
|
||||
|
||||
describe("Copilot SDK usage", () => {
|
||||
it("allows only @github/copilot-sdk", async () => {
|
||||
const repoRoot = path.resolve(process.cwd());
|
||||
const findings = await scanCopilotUsage(repoRoot);
|
||||
|
||||
expect(findings).toEqual([]);
|
||||
});
|
||||
});
|
||||
18
tools/copilot-enforcement/README.md
Normal file
18
tools/copilot-enforcement/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Copilot SDK enforcement
|
||||
|
||||
This tool enforces that the codebase only references the official `@github/copilot-sdk` package.
|
||||
|
||||
## What it checks
|
||||
|
||||
- Imports or requires of `@github/copilot*` packages outside `@github/copilot-sdk`
|
||||
- `package.json` dependency entries that reference `@github/copilot*` packages other than `@github/copilot-sdk`
|
||||
|
||||
## Usage
|
||||
|
||||
Run the check from the repo root:
|
||||
|
||||
```
|
||||
pnpm copilot:check
|
||||
```
|
||||
|
||||
The script exits with a non-zero status if any non-SDK usage is found.
|
||||
186
tools/copilot-enforcement/check-copilot-usage.ts
Normal file
186
tools/copilot-enforcement/check-copilot-usage.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
type FindingKind = "import" | "dependency";
|
||||
|
||||
type Finding = {
|
||||
file: string;
|
||||
kind: FindingKind;
|
||||
specifier: string;
|
||||
line: number | null;
|
||||
};
|
||||
|
||||
const CODE_EXTENSIONS = new Set([
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".mts",
|
||||
".cts",
|
||||
]);
|
||||
|
||||
const EXCLUDED_DIRS = new Set([
|
||||
".git",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"vendor",
|
||||
"coverage",
|
||||
".turbo",
|
||||
".next",
|
||||
"build",
|
||||
]);
|
||||
|
||||
const COPILOT_IMPORT_PATTERN = /(?:from\s+|require\(|import\()\s*["'](@github\/copilot[^"']*)["']/g;
|
||||
const COPILOT_SIDE_EFFECT_PATTERN = /import\s*["'](@github\/copilot[^"']*)["']/g;
|
||||
|
||||
export async function scanCopilotUsage(rootDir: string): Promise<Finding[]> {
|
||||
const findings: Finding[] = [];
|
||||
|
||||
await walkDirectory(rootDir, async (filePath) => {
|
||||
const relativePath = path.relative(rootDir, filePath);
|
||||
|
||||
if (path.basename(filePath) === "package.json") {
|
||||
const packageFindings = await scanPackageJson(filePath, relativePath);
|
||||
findings.push(...packageFindings);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CODE_EXTENSIONS.has(path.extname(filePath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await readFile(filePath, "utf8");
|
||||
findings.push(...scanSourceFile(content, relativePath));
|
||||
});
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
async function walkDirectory(
|
||||
dirPath: string,
|
||||
onFile: (filePath: string) => Promise<void>
|
||||
): Promise<void> {
|
||||
const entries = await readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (EXCLUDED_DIRS.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
await walkDirectory(path.join(dirPath, entry.name), onFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile()) {
|
||||
await onFile(path.join(dirPath, entry.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanSourceFile(content: string, relativePath: string): Finding[] {
|
||||
const findings: Finding[] = [];
|
||||
|
||||
for (const pattern of [COPILOT_IMPORT_PATTERN, COPILOT_SIDE_EFFECT_PATTERN]) {
|
||||
for (const match of content.matchAll(pattern)) {
|
||||
const specifier = match[1];
|
||||
if (specifier === "@github/copilot-sdk") {
|
||||
continue;
|
||||
}
|
||||
if (specifier.startsWith("@github/copilot")) {
|
||||
findings.push({
|
||||
file: relativePath,
|
||||
kind: "import",
|
||||
specifier,
|
||||
line: getLineNumber(content, match.index ?? 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
async function scanPackageJson(
|
||||
filePath: string,
|
||||
relativePath: string
|
||||
): Promise<Finding[]> {
|
||||
const findings: Finding[] = [];
|
||||
const content = await readFile(filePath, "utf8");
|
||||
|
||||
try {
|
||||
const json = JSON.parse(content) as {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
peerDependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const dependencyBlocks = [
|
||||
json.dependencies,
|
||||
json.devDependencies,
|
||||
json.peerDependencies,
|
||||
json.optionalDependencies,
|
||||
];
|
||||
|
||||
for (const block of dependencyBlocks) {
|
||||
if (!block) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const name of Object.keys(block)) {
|
||||
if (name === "@github/copilot-sdk") {
|
||||
continue;
|
||||
}
|
||||
if (name.startsWith("@github/copilot")) {
|
||||
findings.push({
|
||||
file: relativePath,
|
||||
kind: "dependency",
|
||||
specifier: name,
|
||||
line: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return findings;
|
||||
}
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
function getLineNumber(content: string, index: number): number {
|
||||
return content.slice(0, index).split("\n").length;
|
||||
}
|
||||
|
||||
function formatFindings(findings: Finding[]): string {
|
||||
return findings
|
||||
.map((finding) => {
|
||||
const line = finding.line ? `:${finding.line}` : "";
|
||||
return `${finding.file}${line} - ${finding.kind} ${finding.specifier}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function runCli(): Promise<void> {
|
||||
const rootDir = process.cwd();
|
||||
const findings = await scanCopilotUsage(rootDir);
|
||||
|
||||
if (findings.length === 0) {
|
||||
console.log("Copilot SDK check passed.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Non-SDK Copilot usage detected:\n" + formatFindings(findings));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
const isCli =
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
|
||||
if (isCli) {
|
||||
runCli();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user