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": {
|
"colors": {
|
||||||
"primary": "#FF5A36"
|
"primary": "#FF5A36"
|
||||||
},
|
},
|
||||||
"topbarLinks": [
|
"navbar": {
|
||||||
{
|
"links": [
|
||||||
"name": "GitHub",
|
{
|
||||||
"url": "https://github.com/clawdbot/clawdbot"
|
"label": "GitHub",
|
||||||
},
|
"href": "https://github.com/clawdbot/clawdbot"
|
||||||
{
|
},
|
||||||
"name": "Releases",
|
{
|
||||||
"url": "https://github.com/clawdbot/clawdbot/releases"
|
"label": "Releases",
|
||||||
}
|
"href": "https://github.com/clawdbot/clawdbot/releases"
|
||||||
],
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"redirects": [
|
"redirects": [
|
||||||
{
|
{
|
||||||
"source": "/cron",
|
"source": "/cron",
|
||||||
@ -823,6 +825,13 @@
|
|||||||
"help/faq"
|
"help/faq"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Developer",
|
||||||
|
"pages": [
|
||||||
|
"developer-guide",
|
||||||
|
"validation/copilot-usage"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Install & Updates",
|
"group": "Install & Updates",
|
||||||
"pages": [
|
"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:
|
read_when:
|
||||||
- You want to use GitHub Copilot as a model provider
|
- 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
|
# Github Copilot
|
||||||
|
|
||||||
@ -10,35 +10,25 @@ read_when:
|
|||||||
|
|
||||||
GitHub Copilot is GitHub's AI coding assistant. It provides access to Copilot
|
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
|
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`)
|
Clawdbot integrates with GitHub Copilot through the official Copilot CLI SDK.
|
||||||
|
This requires the Copilot CLI to be installed and authenticated on the gateway
|
||||||
Use the native device-login flow to obtain a GitHub token, then exchange it for
|
host.
|
||||||
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.
|
|
||||||
|
|
||||||
## CLI setup
|
## CLI setup
|
||||||
|
|
||||||
```bash
|
```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
|
Then validate Copilot in Clawdbot:
|
||||||
open until it completes.
|
|
||||||
|
```bash
|
||||||
|
clawdbot models auth login-github-copilot
|
||||||
|
```
|
||||||
|
|
||||||
### Optional flags
|
### Optional flags
|
||||||
|
|
||||||
@ -66,5 +56,4 @@ clawdbot models set github-copilot/gpt-4o
|
|||||||
- Requires an interactive TTY; run it directly in a terminal.
|
- Requires an interactive TTY; run it directly in a terminal.
|
||||||
- Copilot model availability depends on your plan; if a model is rejected, try
|
- Copilot model availability depends on your plan; if a model is rejected, try
|
||||||
another ID (for example `github-copilot/gpt-4.1`).
|
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 CLI must stay available on the gateway host.
|
||||||
Copilot API token when Clawdbot runs.
|
|
||||||
|
|||||||
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.
|
This plugin has been deprecated. Use the official Copilot SDK integration instead.
|
||||||
|
|
||||||
## 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`.
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "copilot-proxy",
|
"id": "copilot-proxy",
|
||||||
"providers": [
|
"providers": [],
|
||||||
"copilot-proxy"
|
|
||||||
],
|
|
||||||
"configSchema": {
|
"configSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"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 = {
|
const copilotProxyPlugin = {
|
||||||
id: "copilot-proxy",
|
id: "copilot-proxy",
|
||||||
name: "Copilot Proxy",
|
name: "Copilot Proxy (removed)",
|
||||||
description: "Local Copilot Proxy (VS Code LM) provider plugin",
|
description: "Deprecated: use the official Copilot SDK integration instead.",
|
||||||
configSchema: emptyPluginConfigSchema(),
|
register() {
|
||||||
register(api) {
|
// Intentionally empty: copilot-proxy support has been removed.
|
||||||
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.",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
"name": "@clawdbot/copilot-proxy",
|
"name": "@clawdbot/copilot-proxy",
|
||||||
"version": "2026.1.25",
|
"version": "2026.1.25",
|
||||||
"type": "module",
|
"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": {
|
"clawdbot": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"./index.ts"
|
"./index.ts"
|
||||||
|
|||||||
@ -107,6 +107,7 @@
|
|||||||
"mac:restart": "bash scripts/restart-mac.sh",
|
"mac:restart": "bash scripts/restart-mac.sh",
|
||||||
"mac:package": "bash scripts/package-mac-app.sh",
|
"mac:package": "bash scripts/package-mac-app.sh",
|
||||||
"mac:open": "open dist/Clawdbot.app",
|
"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": "oxlint --type-aware src test",
|
||||||
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
|
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
|
||||||
"lint:all": "pnpm lint && pnpm lint:swift",
|
"lint:all": "pnpm lint && pnpm lint:swift",
|
||||||
@ -156,6 +157,7 @@
|
|||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@grammyjs/runner": "^2.0.3",
|
"@grammyjs/runner": "^2.0.3",
|
||||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||||
|
"@github/copilot-sdk": "^0.1.18",
|
||||||
"@homebridge/ciao": "^1.3.4",
|
"@homebridge/ciao": "^1.3.4",
|
||||||
"@line/bot-sdk": "^10.6.0",
|
"@line/bot-sdk": "^10.6.0",
|
||||||
"@lydell/node-pty": "1.2.0-beta.3",
|
"@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':
|
'@clack/prompts':
|
||||||
specifier: ^0.11.0
|
specifier: ^0.11.0
|
||||||
version: 0.11.0
|
version: 0.11.0
|
||||||
|
'@github/copilot-sdk':
|
||||||
|
specifier: ^0.1.18
|
||||||
|
version: 0.1.18
|
||||||
'@grammyjs/runner':
|
'@grammyjs/runner':
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(grammy@1.39.3)
|
version: 2.0.3(grammy@1.39.3)
|
||||||
@ -357,8 +360,8 @@ importers:
|
|||||||
extensions/memory-core:
|
extensions/memory-core:
|
||||||
dependencies:
|
dependencies:
|
||||||
clawdbot:
|
clawdbot:
|
||||||
specifier: '>=2026.1.25'
|
specifier: '>=2026.1.24'
|
||||||
version: link:../..
|
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:
|
extensions/memory-lancedb:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -982,6 +985,50 @@ packages:
|
|||||||
'@eshaz/web-worker@1.2.2':
|
'@eshaz/web-worker@1.2.2':
|
||||||
resolution: {integrity: sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==}
|
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':
|
'@glideapps/ts-necessities@2.2.3':
|
||||||
resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==}
|
resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==}
|
||||||
|
|
||||||
@ -3124,6 +3171,11 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
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:
|
cli-cursor@5.0.0:
|
||||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -5355,6 +5407,10 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vscode-jsonrpc@8.2.1:
|
||||||
|
resolution: {integrity: sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3:
|
web-streams-polyfill@3.3.3:
|
||||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -6496,6 +6552,39 @@ snapshots:
|
|||||||
'@eshaz/web-worker@1.2.2':
|
'@eshaz/web-worker@1.2.2':
|
||||||
optional: true
|
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': {}
|
'@glideapps/ts-necessities@2.2.3': {}
|
||||||
|
|
||||||
'@google/genai@1.34.0':
|
'@google/genai@1.34.0':
|
||||||
@ -8862,6 +8951,84 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
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:
|
cli-cursor@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 5.1.0
|
restore-cursor: 5.1.0
|
||||||
@ -11374,6 +11541,8 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
vscode-jsonrpc@8.2.1: {}
|
||||||
|
|
||||||
web-streams-polyfill@3.3.3: {}
|
web-streams-polyfill@3.3.3: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
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 {
|
export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean {
|
||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
|
if (normalized === "github-copilot") return true;
|
||||||
if (normalized === "claude-cli") return true;
|
if (normalized === "claude-cli") return true;
|
||||||
if (normalized === "codex-cli") return true;
|
if (normalized === "codex-cli") return true;
|
||||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||||
return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
|
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 {
|
function normalizeAnthropicModelId(model: string): string {
|
||||||
const trimmed = model.trim();
|
const trimmed = model.trim();
|
||||||
if (!trimmed) return trimmed;
|
if (!trimmed) return trimmed;
|
||||||
|
|||||||
@ -427,7 +427,11 @@ export async function resolveImplicitCopilotProvider(params: {
|
|||||||
}): Promise<ProviderConfig | null> {
|
}): Promise<ProviderConfig | null> {
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
|
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 envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
||||||
const githubToken = (envToken ?? "").trim();
|
const githubToken = (envToken ?? "").trim();
|
||||||
|
|
||||||
@ -437,7 +441,7 @@ export async function resolveImplicitCopilotProvider(params: {
|
|||||||
if (!selectedGithubToken && hasProfile) {
|
if (!selectedGithubToken && hasProfile) {
|
||||||
// Use the first available profile as a default for discovery (it will be
|
// Use the first available profile as a default for discovery (it will be
|
||||||
// re-resolved per-run by the embedded runner).
|
// 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;
|
const profile = profileId ? authStore.profiles[profileId] : undefined;
|
||||||
if (profile && profile.type === "token") {
|
if (profile && profile.type === "token") {
|
||||||
selectedGithubToken = profile.token;
|
selectedGithubToken = profile.token;
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import crypto from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
import { resolveAgentModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||||
import { runCliAgent } from "../../agents/cli-runner.js";
|
import { runCliAgent } from "../../agents/cli-runner.js";
|
||||||
|
import { runCopilotSdkAgent } from "../../agents/copilot-sdk-runner.js";
|
||||||
import { getCliSessionId } from "../../agents/cli-session.js";
|
import { getCliSessionId } from "../../agents/cli-session.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.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 { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||||
import {
|
import {
|
||||||
isCompactionFailureError,
|
isCompactionFailureError,
|
||||||
@ -150,6 +151,60 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
thinkLevel: params.followupRun.run.thinkLevel,
|
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)) {
|
if (isCliProvider(provider, params.followupRun.run.config)) {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
|
|||||||
@ -82,8 +82,8 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
|||||||
{
|
{
|
||||||
value: "copilot",
|
value: "copilot",
|
||||||
label: "Copilot",
|
label: "Copilot",
|
||||||
hint: "GitHub + local proxy",
|
hint: "GitHub Copilot CLI",
|
||||||
choices: ["github-copilot", "copilot-proxy"],
|
choices: ["github-copilot"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "openrouter",
|
value: "openrouter",
|
||||||
@ -204,8 +204,8 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
});
|
});
|
||||||
options.push({
|
options.push({
|
||||||
value: "github-copilot",
|
value: "github-copilot",
|
||||||
label: "GitHub Copilot (GitHub device login)",
|
label: "GitHub Copilot (Copilot CLI)",
|
||||||
hint: "Uses GitHub device flow",
|
hint: "Uses the official Copilot CLI + SDK",
|
||||||
});
|
});
|
||||||
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
||||||
options.push({
|
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: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
|
||||||
options.push({ value: "qwen-portal", label: "Qwen OAuth" });
|
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" });
|
options.push({ value: "apiKey", label: "Anthropic API key" });
|
||||||
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
||||||
options.push({
|
options.push({
|
||||||
|
|||||||
@ -1,14 +1,2 @@
|
|||||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
// Deprecated: copilot-proxy integration removed in favor of the official Copilot SDK.
|
||||||
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
|
export {};
|
||||||
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export async function applyAuthChoiceGitHubCopilot(
|
|||||||
|
|
||||||
await params.prompter.note(
|
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.",
|
"Requires an active GitHub Copilot subscription.",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"GitHub Copilot",
|
"GitHub Copilot",
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import type { RuntimeEnv } from "../runtime.js";
|
|||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
|
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
|
||||||
import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.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 { applyAuthChoiceGitHubCopilot } from "./auth-choice.apply.github-copilot.js";
|
||||||
import { applyAuthChoiceGoogleAntigravity } from "./auth-choice.apply.google-antigravity.js";
|
import { applyAuthChoiceGoogleAntigravity } from "./auth-choice.apply.google-antigravity.js";
|
||||||
import { applyAuthChoiceGoogleGeminiCli } from "./auth-choice.apply.google-gemini-cli.js";
|
import { applyAuthChoiceGoogleGeminiCli } from "./auth-choice.apply.google-gemini-cli.js";
|
||||||
@ -44,7 +43,6 @@ export async function applyAuthChoice(
|
|||||||
applyAuthChoiceGitHubCopilot,
|
applyAuthChoiceGitHubCopilot,
|
||||||
applyAuthChoiceGoogleAntigravity,
|
applyAuthChoiceGoogleAntigravity,
|
||||||
applyAuthChoiceGoogleGeminiCli,
|
applyAuthChoiceGoogleGeminiCli,
|
||||||
applyAuthChoiceCopilotProxy,
|
|
||||||
applyAuthChoiceQwenPortal,
|
applyAuthChoiceQwenPortal,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,6 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
|||||||
"synthetic-api-key": "synthetic",
|
"synthetic-api-key": "synthetic",
|
||||||
"venice-api-key": "venice",
|
"venice-api-key": "venice",
|
||||||
"github-copilot": "github-copilot",
|
"github-copilot": "github-copilot",
|
||||||
"copilot-proxy": "copilot-proxy",
|
|
||||||
"minimax-cloud": "minimax",
|
"minimax-cloud": "minimax",
|
||||||
"minimax-api": "minimax",
|
"minimax-api": "minimax",
|
||||||
"minimax-api-lightning": "minimax",
|
"minimax-api-lightning": "minimax",
|
||||||
|
|||||||
@ -29,7 +29,6 @@ export type AuthChoice =
|
|||||||
| "minimax-api-lightning"
|
| "minimax-api-lightning"
|
||||||
| "opencode-zen"
|
| "opencode-zen"
|
||||||
| "github-copilot"
|
| "github-copilot"
|
||||||
| "copilot-proxy"
|
|
||||||
| "qwen-portal"
|
| "qwen-portal"
|
||||||
| "skip";
|
| "skip";
|
||||||
export type GatewayAuthChoice = "off" | "token" | "password";
|
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-antigravity-auth", providerId: "google-antigravity" },
|
||||||
{ pluginId: "google-gemini-cli-auth", providerId: "google-gemini-cli" },
|
{ pluginId: "google-gemini-cli-auth", providerId: "google-gemini-cli" },
|
||||||
{ pluginId: "qwen-portal-auth", providerId: "qwen-portal" },
|
{ pluginId: "qwen-portal-auth", providerId: "qwen-portal" },
|
||||||
{ pluginId: "copilot-proxy", providerId: "copilot-proxy" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
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 () => {
|
it("uses the last non-empty payload for delivery", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const workspaceDir = path.join(tmpDir, "workspace");
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
try {
|
try {
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
|
workspace: workspaceDir,
|
||||||
heartbeat: { every: "5m", target: "whatsapp" },
|
heartbeat: { every: "5m", target: "whatsapp" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -455,12 +458,15 @@ describe("runHeartbeatOnce", () => {
|
|||||||
it("runs heartbeats in the explicit session key when configured", async () => {
|
it("runs heartbeats in the explicit session key when configured", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const workspaceDir = path.join(tmpDir, "workspace");
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
try {
|
try {
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
const groupId = "120363401234567890@g.us";
|
const groupId = "120363401234567890@g.us";
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
|
workspace: workspaceDir,
|
||||||
heartbeat: {
|
heartbeat: {
|
||||||
every: "5m",
|
every: "5m",
|
||||||
target: "last",
|
target: "last",
|
||||||
@ -592,11 +598,14 @@ describe("runHeartbeatOnce", () => {
|
|||||||
it("can include reasoning payloads when enabled", async () => {
|
it("can include reasoning payloads when enabled", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const workspaceDir = path.join(tmpDir, "workspace");
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
try {
|
try {
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
|
workspace: workspaceDir,
|
||||||
heartbeat: {
|
heartbeat: {
|
||||||
every: "5m",
|
every: "5m",
|
||||||
target: "whatsapp",
|
target: "whatsapp",
|
||||||
@ -663,11 +672,14 @@ describe("runHeartbeatOnce", () => {
|
|||||||
it("delivers reasoning even when the main heartbeat reply is HEARTBEAT_OK", async () => {
|
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 tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const workspaceDir = path.join(tmpDir, "workspace");
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
try {
|
try {
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
|
workspace: workspaceDir,
|
||||||
heartbeat: {
|
heartbeat: {
|
||||||
every: "5m",
|
every: "5m",
|
||||||
target: "whatsapp",
|
target: "whatsapp",
|
||||||
@ -733,11 +745,13 @@ describe("runHeartbeatOnce", () => {
|
|||||||
it("loads the default agent session from templated stores", async () => {
|
it("loads the default agent session from templated stores", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions.json");
|
const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions.json");
|
||||||
|
const workspaceDir = path.join(tmpDir, "workspace");
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
try {
|
try {
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: { heartbeat: { every: "5m" } },
|
defaults: { workspace: workspaceDir, heartbeat: { every: "5m" } },
|
||||||
list: [{ id: "work", default: true }],
|
list: [{ id: "work", default: true }],
|
||||||
},
|
},
|
||||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||||
|
|||||||
@ -1,119 +1,15 @@
|
|||||||
|
import { CopilotClient } from "@github/copilot-sdk";
|
||||||
import { intro, note, outro, spinner } from "@clack/prompts";
|
import { intro, note, outro, spinner } from "@clack/prompts";
|
||||||
|
|
||||||
import { ensureAuthProfileStore, upsertAuthProfile } from "../agents/auth-profiles.js";
|
import { ensureAuthProfileStore, upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||||
import { updateConfig } from "../commands/models/shared.js";
|
import { updateConfig } from "../commands/models/shared.js";
|
||||||
import { applyAuthProfileConfig } from "../commands/onboard-auth.js";
|
import { applyAuthProfileConfig } from "../commands/onboard-auth.js";
|
||||||
import { logConfigUpdated } from "../config/logging.js";
|
import { logConfigUpdated } from "../config/logging.js";
|
||||||
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||||
|
|
||||||
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
const COPILOT_LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function githubCopilotLoginCommand(
|
export async function githubCopilotLoginCommand(
|
||||||
opts: { profileId?: string; yes?: boolean },
|
opts: { profileId?: string; yes?: boolean },
|
||||||
@ -137,36 +33,36 @@ export async function githubCopilotLoginCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const spin = spinner();
|
const loginSpin = spinner();
|
||||||
spin.start("Requesting device code from GitHub...");
|
loginSpin.start("Starting Copilot CLI login...");
|
||||||
const device = await requestDeviceCode({ scope: "read:user" });
|
const loginResult = await runCommandWithTimeout(["copilot", "auth", "login"], {
|
||||||
spin.stop("Device code ready");
|
timeoutMs: COPILOT_LOGIN_TIMEOUT_MS,
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
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({
|
upsertAuthProfile({
|
||||||
profileId,
|
profileId,
|
||||||
credential: {
|
credential: {
|
||||||
type: "token",
|
type: "token",
|
||||||
provider: "github-copilot",
|
provider: "github-copilot",
|
||||||
token: accessToken,
|
token: "",
|
||||||
// GitHub device flow token doesn't reliably include expiry here.
|
|
||||||
// Leave expires unset; we'll exchange into Copilot token plus expiry later.
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -179,7 +75,7 @@ export async function githubCopilotLoginCommand(
|
|||||||
);
|
);
|
||||||
|
|
||||||
logConfigUpdated(runtime);
|
logConfigUpdated(runtime);
|
||||||
runtime.log(`Auth profile: ${profileId} (github-copilot/token)`);
|
runtime.log(`Auth profile: ${profileId} (github-copilot/cli)`);
|
||||||
|
|
||||||
outro("Done");
|
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