This commit is contained in:
Hector Flores 2026-01-29 19:00:20 +00:00 committed by GitHub
commit b74cfc37cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1132 additions and 89 deletions

View File

@ -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)

View File

@ -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

View File

@ -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 proxys `/v1` endpoint and uses the model list you configure there. Choose the proxys `/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.

View File

@ -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
View File

@ -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: {}

View File

@ -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: {

View File

@ -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();

View File

@ -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";

View 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);
});
});
});

View 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;
}

View 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;
}
}

View 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
View 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
}
}
}

View File

@ -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);
} }