Merge a984252f7a into 4583f88626
This commit is contained in:
commit
b74cfc37cf
@ -14,6 +14,7 @@ Status: beta.
|
|||||||
- Memory Search: allow extra paths for memory indexing. (#3600) Thanks @kira-ariaki.
|
- Memory Search: allow extra paths for memory indexing. (#3600) Thanks @kira-ariaki.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
- Agents: add GitHub Copilot SDK integration for `copilot-cli` backend. Supports programmatic control of the Copilot CLI via `@github/copilot-sdk`.
|
||||||
- Providers: add Venice AI integration; update Moonshot Kimi references to kimi-k2.5; update MiniMax API endpoint/format. (#2762, #3064)
|
- Providers: add Venice AI integration; update Moonshot Kimi references to kimi-k2.5; update MiniMax API endpoint/format. (#2762, #3064)
|
||||||
- Providers: add Xiaomi MiMo (mimo-v2-flash) support and onboarding flow. (#3454) Thanks @WqyJh.
|
- Providers: add Xiaomi MiMo (mimo-v2-flash) support and onboarding flow. (#3454) Thanks @WqyJh.
|
||||||
- Telegram: quote replies, edit-message action, silent sends, sticker support + vision caching, linkPreview toggle, plugin sendPayload support. (#2900, #2394, #2382, #2548, #1700, #1917)
|
- Telegram: quote replies, edit-message action, silent sends, sticker support + vision caching, linkPreview toggle, plugin sendPayload support. (#2900, #2394, #2382, #2548, #1700, #1917)
|
||||||
|
|||||||
@ -200,6 +200,25 @@ Moltbot also ships a default for `codex-cli`:
|
|||||||
- `imageArg: "--image"`
|
- `imageArg: "--image"`
|
||||||
- `sessionMode: "existing"`
|
- `sessionMode: "existing"`
|
||||||
|
|
||||||
|
Moltbot also ships a default for `copilot-cli`:
|
||||||
|
|
||||||
|
- `command: "copilot"`
|
||||||
|
- `sessionMode: "always"`
|
||||||
|
- Uses the `@github/copilot-sdk` for programmatic control via JSON-RPC
|
||||||
|
- Supports models: `gpt-5`, `gpt-4.1`, `gpt-4.1-mini`, `gpt-4o`, `o1`, `o1-mini`, `o3-mini`, `claude-sonnet-4.5`
|
||||||
|
|
||||||
|
To use Copilot CLI:
|
||||||
|
```bash
|
||||||
|
# Install the CLI globally
|
||||||
|
npm install -g @github/copilot
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
copilot auth login
|
||||||
|
|
||||||
|
# Use in Moltbot
|
||||||
|
moltbot agent --message "hi" --model copilot-cli/gpt-4.1
|
||||||
|
```
|
||||||
|
|
||||||
Override only if needed (common: absolute `command` path).
|
Override only if needed (common: absolute `command` path).
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|||||||
@ -3,6 +3,7 @@ summary: "Sign in to GitHub Copilot from Moltbot using the device flow"
|
|||||||
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 `moltbot models auth login-github-copilot` flow
|
- You need the `moltbot models auth login-github-copilot` flow
|
||||||
|
- You want to use the Copilot CLI as a backend
|
||||||
---
|
---
|
||||||
# Github Copilot
|
# Github Copilot
|
||||||
|
|
||||||
@ -10,9 +11,9 @@ 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. Moltbot can use Copilot as a model
|
models for your GitHub account and plan. Moltbot can use Copilot as a model
|
||||||
provider in two different ways.
|
provider in three different ways.
|
||||||
|
|
||||||
## Two ways to use Copilot in Moltbot
|
## Three ways to use Copilot in Moltbot
|
||||||
|
|
||||||
### 1) Built-in GitHub Copilot provider (`github-copilot`)
|
### 1) Built-in GitHub Copilot provider (`github-copilot`)
|
||||||
|
|
||||||
@ -20,7 +21,17 @@ Use the native device-login flow to obtain a GitHub token, then exchange it for
|
|||||||
Copilot API tokens when Moltbot runs. This is the **default** and simplest path
|
Copilot API tokens when Moltbot runs. This is the **default** and simplest path
|
||||||
because it does not require VS Code.
|
because it does not require VS Code.
|
||||||
|
|
||||||
### 2) Copilot Proxy plugin (`copilot-proxy`)
|
### 2) Copilot CLI backend (`copilot-cli`)
|
||||||
|
|
||||||
|
Use the official [GitHub Copilot CLI](https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line)
|
||||||
|
as a CLI backend via the `@github/copilot-sdk`. This provides access to Copilot's
|
||||||
|
coding agent capabilities with tool execution and session persistence.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
- Install the Copilot CLI: `npm install -g @github/copilot`
|
||||||
|
- Authenticate: `copilot auth login`
|
||||||
|
|
||||||
|
### 3) Copilot Proxy plugin (`copilot-proxy`)
|
||||||
|
|
||||||
Use the **Copilot Proxy** VS Code extension as a local bridge. Moltbot talks to
|
Use the **Copilot Proxy** VS Code extension as a local bridge. Moltbot talks to
|
||||||
the proxy’s `/v1` endpoint and uses the model list you configure there. Choose
|
the proxy’s `/v1` endpoint and uses the model list you configure there. Choose
|
||||||
@ -68,3 +79,43 @@ moltbot models set github-copilot/gpt-4o
|
|||||||
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
|
- The login stores a GitHub token in the auth profile store and exchanges it for a
|
||||||
Copilot API token when Moltbot runs.
|
Copilot API token when Moltbot runs.
|
||||||
|
|
||||||
|
## Copilot CLI backend configuration
|
||||||
|
|
||||||
|
To use the Copilot CLI as a backend (instead of the API), configure it in your
|
||||||
|
`moltbot.config.json`:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "copilot-cli/gpt-4.1" },
|
||||||
|
cliBackends: {
|
||||||
|
"copilot-cli": {
|
||||||
|
command: "copilot",
|
||||||
|
// Optional: customize the CLI path
|
||||||
|
// command: "/usr/local/bin/copilot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available models via Copilot CLI
|
||||||
|
|
||||||
|
The Copilot CLI supports the following models (availability depends on your plan):
|
||||||
|
|
||||||
|
- `gpt-5`
|
||||||
|
- `gpt-4.1`
|
||||||
|
- `gpt-4.1-mini`
|
||||||
|
- `gpt-4.1-nano`
|
||||||
|
- `gpt-4o`
|
||||||
|
- `o1`
|
||||||
|
- `o1-mini`
|
||||||
|
- `o3-mini`
|
||||||
|
- `claude-sonnet-4.5`
|
||||||
|
- `claude-sonnet-4`
|
||||||
|
|
||||||
|
The Copilot CLI backend uses the `@github/copilot-sdk` for programmatic control
|
||||||
|
of the CLI via JSON-RPC.
|
||||||
|
|||||||
@ -158,6 +158,7 @@
|
|||||||
"@aws-sdk/client-bedrock": "^3.975.0",
|
"@aws-sdk/client-bedrock": "^3.975.0",
|
||||||
"@buape/carbon": "0.14.0",
|
"@buape/carbon": "0.14.0",
|
||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
|
"@github/copilot-sdk": "^0.1.19",
|
||||||
"@grammyjs/runner": "^2.0.3",
|
"@grammyjs/runner": "^2.0.3",
|
||||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||||
"@homebridge/ciao": "^1.3.4",
|
"@homebridge/ciao": "^1.3.4",
|
||||||
|
|||||||
175
pnpm-lock.yaml
generated
175
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.19
|
||||||
|
version: 0.1.19
|
||||||
'@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)
|
||||||
@ -383,12 +386,12 @@ importers:
|
|||||||
'@microsoft/agents-hosting-extensions-teams':
|
'@microsoft/agents-hosting-extensions-teams':
|
||||||
specifier: ^1.2.2
|
specifier: ^1.2.2
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
moltbot:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../..
|
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
|
moltbot:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../..
|
||||||
proper-lockfile:
|
proper-lockfile:
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
@ -1040,6 +1043,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.19':
|
||||||
|
resolution: {integrity: sha512-h/KvYb6g99v9SurNJGxeXUatmP7GO8KHTAb68GYfmgUqH1EUeN5g0xMUc5lvKxAi7hwj2OxRR73dd37zMMiiiQ==}
|
||||||
|
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==}
|
||||||
|
|
||||||
@ -3214,11 +3261,6 @@ 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-3:
|
|
||||||
resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==}
|
|
||||||
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'}
|
||||||
@ -5457,6 +5499,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'}
|
||||||
@ -6646,6 +6692,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.19':
|
||||||
|
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':
|
||||||
@ -9098,84 +9177,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
clawdbot@2026.1.24-3(@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
|
||||||
@ -11689,6 +11690,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: {}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export const LEGACY_AUTH_FILENAME = "auth.json";
|
|||||||
export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
|
export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
|
||||||
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
|
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
|
||||||
export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli";
|
export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli";
|
||||||
|
export const COPILOT_CLI_PROFILE_ID = "github-copilot:copilot-cli";
|
||||||
|
|
||||||
export const AUTH_STORE_LOCK_OPTIONS = {
|
export const AUTH_STORE_LOCK_OPTIONS = {
|
||||||
retries: {
|
retries: {
|
||||||
|
|||||||
@ -74,6 +74,37 @@ const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
|||||||
serialize: true,
|
serialize: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const COPILOT_MODEL_ALIASES: Record<string, string> = {
|
||||||
|
"gpt-5": "gpt-5",
|
||||||
|
"gpt-4.1": "gpt-4.1",
|
||||||
|
"gpt-4.1-mini": "gpt-4.1-mini",
|
||||||
|
"gpt-4.1-nano": "gpt-4.1-nano",
|
||||||
|
"gpt-4o": "gpt-4o",
|
||||||
|
o1: "o1",
|
||||||
|
"o1-mini": "o1-mini",
|
||||||
|
"o3-mini": "o3-mini",
|
||||||
|
"claude-sonnet-4.5": "claude-sonnet-4.5",
|
||||||
|
"claude-sonnet-4": "claude-sonnet-4",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration for the Copilot CLI backend.
|
||||||
|
*
|
||||||
|
* Note: The Copilot CLI uses the `@github/copilot-sdk` for programmatic control.
|
||||||
|
* This backend config is used for CLI-style invocation patterns, but the actual
|
||||||
|
* execution is handled by the SDK in `copilot-runner.ts`.
|
||||||
|
*/
|
||||||
|
const DEFAULT_COPILOT_BACKEND: CliBackendConfig = {
|
||||||
|
command: "copilot",
|
||||||
|
args: [],
|
||||||
|
output: "text",
|
||||||
|
input: "arg",
|
||||||
|
modelAliases: COPILOT_MODEL_ALIASES,
|
||||||
|
sessionMode: "always",
|
||||||
|
sessionIdFields: ["sessionId", "session_id"],
|
||||||
|
serialize: true,
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeBackendKey(key: string): string {
|
function normalizeBackendKey(key: string): string {
|
||||||
return normalizeProviderId(key);
|
return normalizeProviderId(key);
|
||||||
}
|
}
|
||||||
@ -107,6 +138,7 @@ export function resolveCliBackendIds(cfg?: MoltbotConfig): Set<string> {
|
|||||||
const ids = new Set<string>([
|
const ids = new Set<string>([
|
||||||
normalizeBackendKey("claude-cli"),
|
normalizeBackendKey("claude-cli"),
|
||||||
normalizeBackendKey("codex-cli"),
|
normalizeBackendKey("codex-cli"),
|
||||||
|
normalizeBackendKey("copilot-cli"),
|
||||||
]);
|
]);
|
||||||
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||||
for (const key of Object.keys(configured)) {
|
for (const key of Object.keys(configured)) {
|
||||||
@ -135,6 +167,12 @@ export function resolveCliBackendConfig(
|
|||||||
if (!command) return null;
|
if (!command) return null;
|
||||||
return { id: normalized, config: { ...merged, command } };
|
return { id: normalized, config: { ...merged, command } };
|
||||||
}
|
}
|
||||||
|
if (normalized === "copilot-cli") {
|
||||||
|
const merged = mergeBackendConfig(DEFAULT_COPILOT_BACKEND, override);
|
||||||
|
const command = merged.command?.trim();
|
||||||
|
if (!command) return null;
|
||||||
|
return { id: normalized, config: { ...merged, command } };
|
||||||
|
}
|
||||||
|
|
||||||
if (!override) return null;
|
if (!override) return null;
|
||||||
const command = override.command?.trim();
|
const command = override.command?.trim();
|
||||||
|
|||||||
@ -26,9 +26,11 @@ import {
|
|||||||
resolveSystemPromptUsage,
|
resolveSystemPromptUsage,
|
||||||
writeCliImages,
|
writeCliImages,
|
||||||
} from "./cli-runner/helpers.js";
|
} from "./cli-runner/helpers.js";
|
||||||
|
import { runCopilotCliAgent, isCopilotCliAvailable } from "./copilot-runner.js";
|
||||||
import { FailoverError, resolveFailoverStatus } from "./failover-error.js";
|
import { FailoverError, resolveFailoverStatus } from "./failover-error.js";
|
||||||
import { classifyFailoverReason, isFailoverErrorMessage } from "./pi-embedded-helpers.js";
|
import { classifyFailoverReason, isFailoverErrorMessage } from "./pi-embedded-helpers.js";
|
||||||
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||||
|
import { normalizeProviderId } from "./model-selection.js";
|
||||||
|
|
||||||
const log = createSubsystemLogger("agent/claude-cli");
|
const log = createSubsystemLogger("agent/claude-cli");
|
||||||
|
|
||||||
@ -50,6 +52,29 @@ export async function runCliAgent(params: {
|
|||||||
cliSessionId?: string;
|
cliSessionId?: string;
|
||||||
images?: ImageContent[];
|
images?: ImageContent[];
|
||||||
}): Promise<EmbeddedPiRunResult> {
|
}): Promise<EmbeddedPiRunResult> {
|
||||||
|
// Route copilot-cli requests to the SDK-based runner
|
||||||
|
const normalizedProvider = normalizeProviderId(params.provider);
|
||||||
|
if (normalizedProvider === "copilot-cli") {
|
||||||
|
return runCopilotCliAgent({
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
sessionFile: params.sessionFile,
|
||||||
|
workspaceDir: params.workspaceDir,
|
||||||
|
config: params.config,
|
||||||
|
prompt: params.prompt,
|
||||||
|
provider: params.provider,
|
||||||
|
model: params.model,
|
||||||
|
thinkLevel: params.thinkLevel,
|
||||||
|
timeoutMs: params.timeoutMs,
|
||||||
|
runId: params.runId,
|
||||||
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
|
streamParams: params.streamParams,
|
||||||
|
ownerNumbers: params.ownerNumbers,
|
||||||
|
cliSessionId: params.cliSessionId,
|
||||||
|
images: params.images,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
const workspaceDir = resolvedWorkspace;
|
const workspaceDir = resolvedWorkspace;
|
||||||
@ -333,3 +358,6 @@ export async function runClaudeCliAgent(params: {
|
|||||||
images: params.images,
|
images: params.images,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-export Copilot-specific utilities
|
||||||
|
export { isCopilotCliAvailable } from "./copilot-runner.js";
|
||||||
|
|||||||
140
src/agents/copilot-credentials.test.ts
Normal file
140
src/agents/copilot-credentials.test.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isCopilotCliInstalled,
|
||||||
|
readCopilotAuthStatus,
|
||||||
|
readCopilotAuthStatusCached,
|
||||||
|
resetCopilotCredentialCacheForTest,
|
||||||
|
} from "./copilot-credentials.js";
|
||||||
|
|
||||||
|
describe("copilot-credentials", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetCopilotCredentialCacheForTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isCopilotCliInstalled", () => {
|
||||||
|
it("returns true when copilot --version succeeds", () => {
|
||||||
|
const execSync = vi.fn().mockReturnValue("0.0.1");
|
||||||
|
const result = isCopilotCliInstalled({ execSync });
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(execSync).toHaveBeenCalledWith("copilot --version", expect.any(Object));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when copilot --version fails", () => {
|
||||||
|
const execSync = vi.fn().mockImplementation(() => {
|
||||||
|
throw new Error("ENOENT: not found");
|
||||||
|
});
|
||||||
|
const result = isCopilotCliInstalled({ execSync });
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses custom cliPath when provided", () => {
|
||||||
|
const execSync = vi.fn().mockReturnValue("0.0.1");
|
||||||
|
isCopilotCliInstalled({ cliPath: "/usr/local/bin/copilot", execSync });
|
||||||
|
expect(execSync).toHaveBeenCalledWith("/usr/local/bin/copilot --version", expect.any(Object));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readCopilotAuthStatus", () => {
|
||||||
|
it("returns authenticated status when CLI reports authenticated", () => {
|
||||||
|
const execSync = vi.fn().mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
isAuthenticated: true,
|
||||||
|
user: {
|
||||||
|
login: "testuser",
|
||||||
|
avatarUrl: "https://example.com/avatar.png",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const result = readCopilotAuthStatus({ execSync });
|
||||||
|
expect(result).toEqual({
|
||||||
|
authenticated: true,
|
||||||
|
login: "testuser",
|
||||||
|
avatarUrl: "https://example.com/avatar.png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns not authenticated when CLI reports not authenticated", () => {
|
||||||
|
const execSync = vi.fn().mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
isAuthenticated: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const result = readCopilotAuthStatus({ execSync });
|
||||||
|
expect(result).toEqual({ authenticated: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when CLI is not installed", () => {
|
||||||
|
const execSync = vi.fn().mockImplementation(() => {
|
||||||
|
const error = new Error("ENOENT: not found");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
const result = readCopilotAuthStatus({ execSync });
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns not authenticated on stderr indicating not logged in", () => {
|
||||||
|
const execSync = vi.fn().mockImplementation(() => {
|
||||||
|
const error = new Error("Command failed") as Error & { stderr: string };
|
||||||
|
error.stderr = "You are not logged in";
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
const result = readCopilotAuthStatus({ execSync });
|
||||||
|
expect(result).toEqual({ authenticated: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readCopilotAuthStatusCached", () => {
|
||||||
|
it("returns cached value within TTL", () => {
|
||||||
|
const execSync = vi.fn().mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
isAuthenticated: true,
|
||||||
|
user: { login: "testuser" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// First call populates cache
|
||||||
|
const result1 = readCopilotAuthStatusCached({ execSync, ttlMs: 60000 });
|
||||||
|
expect(result1?.authenticated).toBe(true);
|
||||||
|
expect(execSync).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Second call should use cache
|
||||||
|
const result2 = readCopilotAuthStatusCached({ execSync, ttlMs: 60000 });
|
||||||
|
expect(result2?.authenticated).toBe(true);
|
||||||
|
expect(execSync).toHaveBeenCalledTimes(1); // Not called again
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refreshes cache after TTL expires", async () => {
|
||||||
|
const execSync = vi.fn().mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
isAuthenticated: true,
|
||||||
|
user: { login: "testuser" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// First call with very short TTL
|
||||||
|
readCopilotAuthStatusCached({ execSync, ttlMs: 1 });
|
||||||
|
expect(execSync).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Wait for TTL to expire
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
|
||||||
|
// Second call should refresh
|
||||||
|
readCopilotAuthStatusCached({ execSync, ttlMs: 1 });
|
||||||
|
expect(execSync).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not cache when ttlMs is 0", () => {
|
||||||
|
const execSync = vi.fn().mockReturnValue(
|
||||||
|
JSON.stringify({
|
||||||
|
isAuthenticated: true,
|
||||||
|
user: { login: "testuser" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
readCopilotAuthStatusCached({ execSync, ttlMs: 0 });
|
||||||
|
readCopilotAuthStatusCached({ execSync, ttlMs: 0 });
|
||||||
|
expect(execSync).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
168
src/agents/copilot-credentials.ts
Normal file
168
src/agents/copilot-credentials.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Credential management for the GitHub Copilot CLI.
|
||||||
|
*
|
||||||
|
* This module provides functions to check the authentication status of the Copilot CLI
|
||||||
|
* and cache the results for efficient repeated access.
|
||||||
|
*/
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
|
||||||
|
const log = createSubsystemLogger("agents/copilot-credentials");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached authentication status from the Copilot CLI.
|
||||||
|
*/
|
||||||
|
export type CopilotCredential = {
|
||||||
|
/** Whether the CLI is authenticated with a valid GitHub account. */
|
||||||
|
authenticated: boolean;
|
||||||
|
/** GitHub login username (if authenticated). */
|
||||||
|
login?: string;
|
||||||
|
/** GitHub avatar URL (if authenticated). */
|
||||||
|
avatarUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CachedValue<T> = {
|
||||||
|
value: T | null;
|
||||||
|
readAt: number;
|
||||||
|
cacheKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let copilotCliCache: CachedValue<CopilotCredential> | null = null;
|
||||||
|
|
||||||
|
/** Reset the cache for testing purposes. */
|
||||||
|
export function resetCopilotCredentialCacheForTest(): void {
|
||||||
|
copilotCliCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecSyncFn = typeof execSync;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the Copilot CLI is installed and available on PATH.
|
||||||
|
*/
|
||||||
|
export function isCopilotCliInstalled(options?: {
|
||||||
|
cliPath?: string;
|
||||||
|
execSync?: ExecSyncFn;
|
||||||
|
}): boolean {
|
||||||
|
const execSyncImpl = options?.execSync ?? execSync;
|
||||||
|
const cliPath = options?.cliPath ?? "copilot";
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSyncImpl(`${cliPath} --version`, {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the authentication status from the Copilot CLI.
|
||||||
|
*
|
||||||
|
* Requires the `copilot` CLI to be installed. If the CLI is not installed
|
||||||
|
* or authentication status cannot be determined, returns null.
|
||||||
|
*/
|
||||||
|
export function readCopilotAuthStatus(options?: {
|
||||||
|
cliPath?: string;
|
||||||
|
execSync?: ExecSyncFn;
|
||||||
|
}): CopilotCredential | null {
|
||||||
|
const execSyncImpl = options?.execSync ?? execSync;
|
||||||
|
const cliPath = options?.cliPath ?? "copilot";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The Copilot CLI has a `copilot auth status --json` command that returns auth info.
|
||||||
|
const result = execSyncImpl(`${cliPath} auth status --json`, {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 10000,
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = JSON.parse(result.trim()) as Record<string, unknown>;
|
||||||
|
|
||||||
|
// The CLI returns { isAuthenticated: boolean, user?: { login, avatarUrl } }
|
||||||
|
const isAuthenticated = data.isAuthenticated === true;
|
||||||
|
const user = data.user as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
log.info("copilot cli is not authenticated");
|
||||||
|
return { authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = typeof user?.login === "string" ? user.login : undefined;
|
||||||
|
const avatarUrl = typeof user?.avatarUrl === "string" ? user.avatarUrl : undefined;
|
||||||
|
|
||||||
|
log.info("read copilot auth status from cli", {
|
||||||
|
authenticated: true,
|
||||||
|
login,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
login,
|
||||||
|
avatarUrl,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Check if it's a "not installed" error vs an auth error
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
if (message.includes("ENOENT") || message.includes("not found")) {
|
||||||
|
log.debug("copilot cli not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse error response - CLI may return JSON even on error
|
||||||
|
try {
|
||||||
|
const stderr =
|
||||||
|
error instanceof Error && "stderr" in error
|
||||||
|
? String((error as { stderr: unknown }).stderr)
|
||||||
|
: "";
|
||||||
|
if (stderr.includes("not logged in") || stderr.includes("not authenticated")) {
|
||||||
|
return { authenticated: false };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("failed to read copilot auth status", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the Copilot CLI authentication status with caching.
|
||||||
|
*
|
||||||
|
* @param options.ttlMs - How long to cache the result in milliseconds (default: no caching)
|
||||||
|
*/
|
||||||
|
export function readCopilotAuthStatusCached(options?: {
|
||||||
|
cliPath?: string;
|
||||||
|
ttlMs?: number;
|
||||||
|
execSync?: ExecSyncFn;
|
||||||
|
}): CopilotCredential | null {
|
||||||
|
const ttlMs = options?.ttlMs ?? 0;
|
||||||
|
const now = Date.now();
|
||||||
|
const cacheKey = options?.cliPath ?? "copilot";
|
||||||
|
|
||||||
|
if (
|
||||||
|
ttlMs > 0 &&
|
||||||
|
copilotCliCache &&
|
||||||
|
copilotCliCache.cacheKey === cacheKey &&
|
||||||
|
now - copilotCliCache.readAt < ttlMs
|
||||||
|
) {
|
||||||
|
return copilotCliCache.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = readCopilotAuthStatus({
|
||||||
|
cliPath: options?.cliPath,
|
||||||
|
execSync: options?.execSync,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ttlMs > 0) {
|
||||||
|
copilotCliCache = { value, readAt: now, cacheKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
157
src/agents/copilot-runner.ts
Normal file
157
src/agents/copilot-runner.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Copilot CLI Runner - runs agent prompts through the GitHub Copilot SDK.
|
||||||
|
*
|
||||||
|
* This module provides a runner that uses the `@github/copilot-sdk` to execute
|
||||||
|
* agent prompts through the Copilot CLI, similar to how `cli-runner.ts` handles
|
||||||
|
* Claude CLI and Codex CLI backends.
|
||||||
|
*/
|
||||||
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
|
import type { MoltbotConfig } 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 { resolveCliBackendConfig } from "./cli-backends.js";
|
||||||
|
import { normalizeCliModel, buildSystemPrompt } from "./cli-runner/helpers.js";
|
||||||
|
import { runCopilotAgent, checkCopilotAvailable } from "./copilot-sdk.js";
|
||||||
|
import { resolveMoltbotDocsPath } from "./docs-path.js";
|
||||||
|
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
|
||||||
|
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
|
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||||
|
|
||||||
|
const log = createSubsystemLogger("agent/copilot-cli");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the Copilot CLI backend is available.
|
||||||
|
*/
|
||||||
|
export function isCopilotCliAvailable(options?: { cliPath?: string }): boolean {
|
||||||
|
const status = checkCopilotAvailable({ cliPath: options?.cliPath });
|
||||||
|
return status.available && status.authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an agent prompt through the Copilot CLI using the SDK.
|
||||||
|
*
|
||||||
|
* This function is designed to match the signature of `runCliAgent` for
|
||||||
|
* compatibility with the existing CLI backend infrastructure.
|
||||||
|
*/
|
||||||
|
export async function runCopilotCliAgent(params: {
|
||||||
|
sessionId: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
sessionFile: string;
|
||||||
|
workspaceDir: string;
|
||||||
|
config?: MoltbotConfig;
|
||||||
|
prompt: string;
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
thinkLevel?: ThinkLevel;
|
||||||
|
timeoutMs: number;
|
||||||
|
runId: string;
|
||||||
|
extraSystemPrompt?: string;
|
||||||
|
streamParams?: import("../commands/agent/types.js").AgentStreamParams;
|
||||||
|
ownerNumbers?: string[];
|
||||||
|
cliSessionId?: string;
|
||||||
|
images?: ImageContent[];
|
||||||
|
}): Promise<EmbeddedPiRunResult> {
|
||||||
|
const started = Date.now();
|
||||||
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
|
const workspaceDir = resolvedWorkspace;
|
||||||
|
const provider = params.provider ?? "copilot-cli";
|
||||||
|
|
||||||
|
const backendResolved = resolveCliBackendConfig(provider, params.config);
|
||||||
|
if (!backendResolved) {
|
||||||
|
throw new Error(`Unknown CLI backend: ${provider}`);
|
||||||
|
}
|
||||||
|
const backend = backendResolved.config;
|
||||||
|
const modelId = (params.model ?? "gpt-4.1").trim() || "gpt-4.1";
|
||||||
|
const normalizedModel = normalizeCliModel(modelId, backend);
|
||||||
|
const modelDisplay = `${provider}/${modelId}`;
|
||||||
|
|
||||||
|
// Build system prompt with context
|
||||||
|
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 resolveMoltbotDocsPath({
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(`copilot-cli exec: model=${normalizedModel} promptChars=${params.prompt.length}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runCopilotAgent({
|
||||||
|
prompt: params.prompt,
|
||||||
|
model: normalizedModel,
|
||||||
|
cliPath: backend.command,
|
||||||
|
cwd: workspaceDir,
|
||||||
|
systemPrompt,
|
||||||
|
timeoutMs: params.timeoutMs,
|
||||||
|
sessionId: params.cliSessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = result.text?.trim();
|
||||||
|
const payloads = text ? [{ text }] : undefined;
|
||||||
|
|
||||||
|
// When resuming a session, the SDK's sessionId should be authoritative.
|
||||||
|
// For new sessions, fall back to params.sessionId if SDK doesn't return one.
|
||||||
|
const resolvedSessionId = params.cliSessionId
|
||||||
|
? result.sessionId // Resuming: use SDK's session ID
|
||||||
|
: (result.sessionId ?? params.sessionId ?? ""); // New: SDK or fallback
|
||||||
|
|
||||||
|
return {
|
||||||
|
payloads,
|
||||||
|
meta: {
|
||||||
|
durationMs: Date.now() - started,
|
||||||
|
agentMeta: {
|
||||||
|
sessionId: resolvedSessionId,
|
||||||
|
provider,
|
||||||
|
model: modelId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
log.error("copilot-cli run failed", { error: message });
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/agents/copilot-sdk.test.ts
Normal file
90
src/agents/copilot-sdk.test.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
import { checkCopilotAvailable } from "./copilot-sdk.js";
|
||||||
|
import * as copilotCredentials from "./copilot-credentials.js";
|
||||||
|
|
||||||
|
vi.mock("./copilot-credentials.js", () => ({
|
||||||
|
isCopilotCliInstalled: vi.fn(),
|
||||||
|
readCopilotAuthStatusCached: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("copilot-sdk", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkCopilotAvailable", () => {
|
||||||
|
it("returns not available when CLI is not installed", () => {
|
||||||
|
vi.mocked(copilotCredentials.isCopilotCliInstalled).mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = checkCopilotAvailable();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
available: false,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns available but not authenticated when CLI is installed but not authenticated", () => {
|
||||||
|
vi.mocked(copilotCredentials.isCopilotCliInstalled).mockReturnValue(true);
|
||||||
|
vi.mocked(copilotCredentials.readCopilotAuthStatusCached).mockReturnValue({
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = checkCopilotAvailable();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
available: true,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns available and authenticated with user info when CLI is authenticated", () => {
|
||||||
|
vi.mocked(copilotCredentials.isCopilotCliInstalled).mockReturnValue(true);
|
||||||
|
vi.mocked(copilotCredentials.readCopilotAuthStatusCached).mockReturnValue({
|
||||||
|
authenticated: true,
|
||||||
|
login: "testuser",
|
||||||
|
avatarUrl: "https://example.com/avatar.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = checkCopilotAvailable();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
available: true,
|
||||||
|
authenticated: true,
|
||||||
|
login: "testuser",
|
||||||
|
avatarUrl: "https://example.com/avatar.png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns available but not authenticated when auth status is null", () => {
|
||||||
|
vi.mocked(copilotCredentials.isCopilotCliInstalled).mockReturnValue(true);
|
||||||
|
vi.mocked(copilotCredentials.readCopilotAuthStatusCached).mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = checkCopilotAvailable();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
available: true,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes cliPath option to credential functions", () => {
|
||||||
|
vi.mocked(copilotCredentials.isCopilotCliInstalled).mockReturnValue(true);
|
||||||
|
vi.mocked(copilotCredentials.readCopilotAuthStatusCached).mockReturnValue({
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
checkCopilotAvailable({ cliPath: "/custom/path/copilot" });
|
||||||
|
|
||||||
|
expect(copilotCredentials.isCopilotCliInstalled).toHaveBeenCalledWith({
|
||||||
|
cliPath: "/custom/path/copilot",
|
||||||
|
});
|
||||||
|
expect(copilotCredentials.readCopilotAuthStatusCached).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
cliPath: "/custom/path/copilot",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
345
src/agents/copilot-sdk.ts
Normal file
345
src/agents/copilot-sdk.ts
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
/**
|
||||||
|
* GitHub Copilot SDK integration for Moltbot.
|
||||||
|
*
|
||||||
|
* This module provides a thin wrapper around the `@github/copilot-sdk` package
|
||||||
|
* for programmatic control of GitHub Copilot CLI via JSON-RPC.
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
CopilotClient,
|
||||||
|
CopilotClientOptions,
|
||||||
|
CopilotSession,
|
||||||
|
SessionConfig,
|
||||||
|
SessionEvent,
|
||||||
|
} from "@github/copilot-sdk";
|
||||||
|
|
||||||
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
import { isCopilotCliInstalled, readCopilotAuthStatusCached } from "./copilot-credentials.js";
|
||||||
|
|
||||||
|
const log = createSubsystemLogger("agents/copilot-sdk");
|
||||||
|
|
||||||
|
// Re-export SDK types that consumers may need.
|
||||||
|
export type { CopilotClient, CopilotClientOptions, CopilotSession, SessionConfig, SessionEvent };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for creating a Moltbot-configured Copilot client.
|
||||||
|
*/
|
||||||
|
export type MoltbotCopilotClientOptions = {
|
||||||
|
/** Path to the Copilot CLI executable (default: "copilot" from PATH). */
|
||||||
|
cliPath?: string;
|
||||||
|
/** Working directory for the CLI process. */
|
||||||
|
cwd?: string;
|
||||||
|
/** Log level for the CLI server. */
|
||||||
|
logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all";
|
||||||
|
/** Auto-restart the CLI server if it crashes (default: true). */
|
||||||
|
autoRestart?: boolean;
|
||||||
|
/** Environment variables to pass to the CLI process. */
|
||||||
|
env?: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the Copilot CLI is available and authenticated.
|
||||||
|
*
|
||||||
|
* @returns Object with `available` and `authenticated` flags, plus optional user info.
|
||||||
|
*/
|
||||||
|
export function checkCopilotAvailable(options?: { cliPath?: string }): {
|
||||||
|
available: boolean;
|
||||||
|
authenticated: boolean;
|
||||||
|
login?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
} {
|
||||||
|
const installed = isCopilotCliInstalled({ cliPath: options?.cliPath });
|
||||||
|
if (!installed) {
|
||||||
|
return { available: false, authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const authStatus = readCopilotAuthStatusCached({
|
||||||
|
cliPath: options?.cliPath,
|
||||||
|
ttlMs: 5 * 60 * 1000, // Cache for 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authStatus) {
|
||||||
|
return { available: true, authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
authenticated: authStatus.authenticated,
|
||||||
|
login: authStatus.login,
|
||||||
|
avatarUrl: authStatus.avatarUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a CopilotClient with Moltbot-specific defaults.
|
||||||
|
*
|
||||||
|
* This is a factory function that lazily imports the SDK and creates
|
||||||
|
* a client instance. The client should be started before use.
|
||||||
|
*/
|
||||||
|
export async function createCopilotClient(
|
||||||
|
options?: MoltbotCopilotClientOptions,
|
||||||
|
): Promise<CopilotClient> {
|
||||||
|
const { CopilotClient: CopilotClientClass } = await import("@github/copilot-sdk");
|
||||||
|
|
||||||
|
const clientOptions: CopilotClientOptions = {
|
||||||
|
cliPath: options?.cliPath ?? "copilot",
|
||||||
|
cwd: options?.cwd,
|
||||||
|
logLevel: options?.logLevel ?? "warning",
|
||||||
|
autoRestart: false, // Disable auto-restart to avoid keeping event loop alive
|
||||||
|
useStdio: true, // Use stdio transport for better process control
|
||||||
|
autoStart: false, // We'll start manually for better error handling
|
||||||
|
env: options?.env,
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info("creating copilot client", {
|
||||||
|
cliPath: clientOptions.cliPath,
|
||||||
|
cwd: clientOptions.cwd,
|
||||||
|
logLevel: clientOptions.logLevel,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new CopilotClientClass(clientOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for running a single-turn Copilot agent interaction.
|
||||||
|
*/
|
||||||
|
export type RunCopilotAgentParams = {
|
||||||
|
/** The prompt/message to send. */
|
||||||
|
prompt: string;
|
||||||
|
/** Model to use (e.g., "gpt-5", "gpt-4.1", "claude-sonnet-4.5"). */
|
||||||
|
model?: string;
|
||||||
|
/** Path to the Copilot CLI executable. */
|
||||||
|
cliPath?: string;
|
||||||
|
/** Working directory for the CLI process. */
|
||||||
|
cwd?: string;
|
||||||
|
/** System message content (appended to CLI defaults). */
|
||||||
|
systemPrompt?: string;
|
||||||
|
/** Timeout in milliseconds (default: 120000). */
|
||||||
|
timeoutMs?: number;
|
||||||
|
/** Session ID to resume (for multi-turn conversations). */
|
||||||
|
sessionId?: string;
|
||||||
|
/** Environment variables to pass to the CLI. */
|
||||||
|
env?: Record<string, string | undefined>;
|
||||||
|
/** Callback for streaming events. */
|
||||||
|
onEvent?: (event: SessionEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from a Copilot agent run.
|
||||||
|
*/
|
||||||
|
export type CopilotAgentResult = {
|
||||||
|
/** The final assistant response text. */
|
||||||
|
text: string;
|
||||||
|
/** Session ID for resuming conversations. */
|
||||||
|
sessionId: string;
|
||||||
|
/** Events received during the run. */
|
||||||
|
events: SessionEvent[];
|
||||||
|
/** Duration in milliseconds. */
|
||||||
|
durationMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single-turn Copilot agent interaction.
|
||||||
|
*
|
||||||
|
* Creates a client, starts a session, sends the prompt, waits for completion,
|
||||||
|
* and cleans up. For multi-turn conversations, pass the returned `sessionId`
|
||||||
|
* back in subsequent calls.
|
||||||
|
*
|
||||||
|
* Note: When resuming a session, model and system prompt settings from the
|
||||||
|
* original session are preserved. New configuration values are not applied
|
||||||
|
* during resumption.
|
||||||
|
*/
|
||||||
|
export async function runCopilotAgent(params: RunCopilotAgentParams): Promise<CopilotAgentResult> {
|
||||||
|
const started = Date.now();
|
||||||
|
const events: SessionEvent[] = [];
|
||||||
|
let finalText = "";
|
||||||
|
|
||||||
|
const client = await createCopilotClient({
|
||||||
|
cliPath: params.cliPath,
|
||||||
|
cwd: params.cwd,
|
||||||
|
env: params.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
let session: CopilotSession | null = null;
|
||||||
|
let unsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start the client
|
||||||
|
await client.start();
|
||||||
|
|
||||||
|
// Configure session for new sessions
|
||||||
|
const sessionConfig: SessionConfig = {
|
||||||
|
model: params.model,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add system message if provided (only applies to new sessions)
|
||||||
|
if (params.systemPrompt) {
|
||||||
|
sessionConfig.systemMessage = {
|
||||||
|
mode: "append",
|
||||||
|
content: params.systemPrompt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or resume session
|
||||||
|
// Note: When resuming, the SDK preserves the original session's model/prompt.
|
||||||
|
// The ResumeSessionConfig type only supports tools, provider, streaming,
|
||||||
|
// onPermissionRequest, mcpServers, customAgents, skillDirectories, disabledSkills.
|
||||||
|
if (params.sessionId) {
|
||||||
|
session = await client.resumeSession(params.sessionId, {
|
||||||
|
streaming: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
session = await client.createSession(sessionConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = session.sessionId;
|
||||||
|
|
||||||
|
// Set up event handler
|
||||||
|
unsubscribe = session.on((event: SessionEvent) => {
|
||||||
|
events.push(event);
|
||||||
|
params.onEvent?.(event);
|
||||||
|
|
||||||
|
// Capture final text from assistant message events
|
||||||
|
if (event.type === "assistant.message") {
|
||||||
|
const data = event.data as { content?: string };
|
||||||
|
if (typeof data.content === "string") {
|
||||||
|
finalText = data.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the message and wait for completion
|
||||||
|
const result = await session.sendAndWait({ prompt: params.prompt }, params.timeoutMs ?? 120000);
|
||||||
|
|
||||||
|
// Extract text from result if available
|
||||||
|
if (result?.data?.content && typeof result.data.content === "string") {
|
||||||
|
finalText = result.data.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: finalText,
|
||||||
|
sessionId,
|
||||||
|
events,
|
||||||
|
durationMs: Date.now() - started,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
// Clean up in order: unsubscribe, abort session, destroy session, stop client
|
||||||
|
// NOTE: Due to a limitation in vscode-jsonrpc (used by @github/copilot-sdk),
|
||||||
|
// the process may not exit cleanly after cleanup. This is acceptable for gateway
|
||||||
|
// usage but means CLI one-off commands will hang. The SDK team should fix this.
|
||||||
|
if (unsubscribe) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort any in-flight request, then destroy the session
|
||||||
|
if (session) {
|
||||||
|
try {
|
||||||
|
await session.abort();
|
||||||
|
} catch {
|
||||||
|
// Ignore abort errors (may not have active request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only destroy session if we created a new one (not resuming)
|
||||||
|
if (!params.sessionId) {
|
||||||
|
try {
|
||||||
|
await session.destroy();
|
||||||
|
} catch {
|
||||||
|
// Ignore destroy errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access internal SDK handles BEFORE cleanup so we can unref them after
|
||||||
|
const clientAny = client as unknown as {
|
||||||
|
cliProcess?: {
|
||||||
|
unref?: () => void;
|
||||||
|
stdin?: { unref?: () => void };
|
||||||
|
stdout?: { unref?: () => void };
|
||||||
|
stderr?: { unref?: () => void };
|
||||||
|
removeAllListeners?: () => void;
|
||||||
|
};
|
||||||
|
socket?: { unref?: () => void };
|
||||||
|
};
|
||||||
|
const cliProcess = clientAny.cliProcess;
|
||||||
|
const socket = clientAny.socket;
|
||||||
|
|
||||||
|
// Stop the client gracefully first, then force if needed
|
||||||
|
try {
|
||||||
|
await client.stop();
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await client.forceStop();
|
||||||
|
} catch {
|
||||||
|
// Ignore forceStop errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unref all handles to allow Node to exit (works around SDK cleanup bug)
|
||||||
|
if (cliProcess) {
|
||||||
|
cliProcess.unref?.();
|
||||||
|
cliProcess.stdin?.unref?.();
|
||||||
|
cliProcess.stdout?.unref?.();
|
||||||
|
cliProcess.stderr?.unref?.();
|
||||||
|
cliProcess.removeAllListeners?.();
|
||||||
|
}
|
||||||
|
if (socket) {
|
||||||
|
socket.unref?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List available Copilot sessions.
|
||||||
|
*/
|
||||||
|
export async function listCopilotSessions(options?: {
|
||||||
|
cliPath?: string;
|
||||||
|
cwd?: string;
|
||||||
|
}): Promise<Array<{ sessionId: string; createdAt?: string }>> {
|
||||||
|
const client = await createCopilotClient({
|
||||||
|
cliPath: options?.cliPath,
|
||||||
|
cwd: options?.cwd,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.start();
|
||||||
|
const sessions = await client.listSessions();
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
sessionId: s.sessionId,
|
||||||
|
createdAt: s.startTime?.toISOString(),
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await client.stop();
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Copilot session.
|
||||||
|
*/
|
||||||
|
export async function deleteCopilotSession(
|
||||||
|
sessionId: string,
|
||||||
|
options?: {
|
||||||
|
cliPath?: string;
|
||||||
|
cwd?: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const client = await createCopilotClient({
|
||||||
|
cliPath: options?.cliPath,
|
||||||
|
cwd: options?.cwd,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.start();
|
||||||
|
await client.deleteSession(sessionId);
|
||||||
|
log.info("deleted copilot session", { sessionId });
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await client.stop();
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ export function isCliProvider(provider: string, cfg?: MoltbotConfig): boolean {
|
|||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
if (normalized === "claude-cli") return true;
|
if (normalized === "claude-cli") return true;
|
||||||
if (normalized === "codex-cli") return true;
|
if (normalized === "codex-cli") return true;
|
||||||
|
if (normalized === "copilot-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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user