@goodandready/dsh-subscriptions 0.6.65 → 0.6.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/README.ru.md +6 -0
- package/lib/client.js +40 -0
- package/lib/config-schema.js +4 -0
- package/lib/diagnostics.js +2 -0
- package/lib/import-auth.js +25 -0
- package/lib/routes/accounts.js +1 -1
- package/lib/vendors/copilot-cli.js +400 -0
- package/lib/vendors/copilot.js +31 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,12 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
207
207
|
* **In-App Toast Alerts (`enableToasts`)**: Lightweight floating toast alerts in the DSH chat interface notifying you when accounts enter quarantine, recover from cooldowns, or exceed quota safety thresholds.
|
|
208
208
|
* **Automated Webhook Integration**: Instant event notifications dispatched via `notifyQuarantineEntered` and `notifyQuarantineReleased`.
|
|
209
209
|
|
|
210
|
+
### 20. 🤖 Native Copilot CLI SDK Mode & Local Tool Calling (v0.6.66)
|
|
211
|
+
* **Direct Copilot CLI Integration (#508)**: Native driver `copilot-cli.js` supporting execution via local `copilot` / `github-copilot-cli` binaries with zero network token emulation risk.
|
|
212
|
+
* **Smart Mode Switching (`copilotMode`)**: Choose between `auto` (prefers local CLI with graceful web API fallback), `cli` (strict CLI SDK execution), and `web` (direct OAuth/API).
|
|
213
|
+
* **Multi-Source Credential Detection**: Automatically discovers tokens from GitHub CLI (`gh hosts.yml`), Copilot config files (`hosts.json`, `apps.json`), and environment variables (`GITHUB_COPILOT_TOKEN`, `GH_TOKEN`).
|
|
214
|
+
* **Full Tool-Calling & Streaming**: Seamless translation of CLI process output into real-time streaming chunks and multi-turn tool-call deltas.
|
|
215
|
+
|
|
210
216
|
### 19. 🧠 Session Reasoning Effort Management & Contract Matrix (v0.6.65)
|
|
211
217
|
* **Session-Scoped Reasoning Effort Control (#505)**: Dynamically adjust reasoning effort (`low`, `medium`, `high`, `max`) or disable reasoning (`off`) per conversation session via REST endpoints (`GET`, `POST`, `DELETE /dsh-subscriptions/session-reasoning`).
|
|
212
218
|
* **Model-Switch Invariant & Safety Guard**: Automatic suppression of reasoning parameters when switching from a reasoning model (e.g. `o1`) to a standard model (e.g. `gpt-4o`), avoiding upstream HTTP 400 parameter errors.
|
package/README.ru.md
CHANGED
|
@@ -183,6 +183,12 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
|
|
|
183
183
|
* **Всплывающие уведомления (Toast Alerts)**: Легковесные всплывающие тосты в веб-интерфейсе DSH при событиях карантина (с отображением причины), восстановлении доступа и критическом расходе квоты. Включение управляется параметром `enableToasts` (по умолчанию `true`).
|
|
184
184
|
* **Интеграция с вебхуками**: Автоматическая отправка уведомлений через `notifyQuarantineEntered` и `notifyQuarantineReleased`.
|
|
185
185
|
|
|
186
|
+
### 16. 🤖 Нативный режим Copilot CLI SDK и локальный Tool Calling (v0.6.66)
|
|
187
|
+
* **Прямая интеграция с Copilot CLI (#508)**: Драйвер `copilot-cli.js` для работы через локальный бинарник `copilot` / `github-copilot-cli` без риска сетевых блокировок при эмуляции токенов.
|
|
188
|
+
* **Гибкие режимы работы (`copilotMode`)**: Поддержка режимов `auto` (приоритет CLI с автоматическим fallback на web-API), `cli` (строгий режим CLI SDK) и `web` (прямое подключение по OAuth/API).
|
|
189
|
+
* **Многоканальное обнаружение учетных данных**: Чтение токенов из `gh hosts.yml`, `hosts.json`, `apps.json` и переменных окружения (`GITHUB_COPILOT_TOKEN`, `GH_TOKEN`).
|
|
190
|
+
* **Потоковая передача и вызовы инструментов**: Полная поддержка real-time стриминга и событий `tool-call-delta` при исполнении через CLI.
|
|
191
|
+
|
|
186
192
|
### 15. 🧠 Управление Reasoning Effort в сессии и матрица контрактов (v0.6.65)
|
|
187
193
|
* **Управление глубиной рассуждений в сессии (#505)**: Динамическая настройка усилия рассуждений (`low`, `medium`, `high`, `max`) или отключение (`off`) в рамках диалога через REST API (`GET`, `POST`, `DELETE /dsh-subscriptions/session-reasoning`).
|
|
188
194
|
* **Безопасность при смене модели**: Автоматическое подавление параметров рассуждений при переключении с reasoning-модели (напр. `o1`) на обычную (напр. `gpt-4o`), предотвращающее ошибки HTTP 400 от upstream API.
|
package/lib/client.js
CHANGED
|
@@ -395,6 +395,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
395
395
|
"codexVerbosityLow": "Low",
|
|
396
396
|
"codexVerbosityMedium": "Medium",
|
|
397
397
|
"codexVerbosityHigh": "High",
|
|
398
|
+
"copilotMode": "Copilot execution mode",
|
|
399
|
+
"copilotModeHint": "Execution mode: auto (prefer local CLI with web fallback), cli (strict CLI SDK), or web (direct API).",
|
|
400
|
+
"copilotModeAuto": "Auto (CLI with Web fallback)",
|
|
401
|
+
"copilotModeCli": "CLI Only (Local copilot-cli)",
|
|
402
|
+
"copilotModeWeb": "Web Only (Direct API)",
|
|
403
|
+
"copilotCliPath": "Custom Copilot CLI path",
|
|
404
|
+
"copilotCliPathHint": "Optional path to copilot or github-copilot-cli binary.",
|
|
398
405
|
"ollamaFallback": "Ollama local fallback",
|
|
399
406
|
"ollamaFallbackHint": "Automatically route queries to a local Ollama instance when cloud quota is exhausted.",
|
|
400
407
|
"ollamaBaseUrl": "Ollama Base URL",
|
|
@@ -660,6 +667,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
660
667
|
"codexVerbosityLow": "低",
|
|
661
668
|
"codexVerbosityMedium": "中",
|
|
662
669
|
"codexVerbosityHigh": "高",
|
|
670
|
+
"copilotMode": "Copilot 执行模式",
|
|
671
|
+
"copilotModeHint": "执行模式:auto(优先本地 CLI,带 Web 回退)、cli(仅本地 CLI)、web(仅直连 API)。",
|
|
672
|
+
"copilotModeAuto": "自动(优先本地 CLI)",
|
|
673
|
+
"copilotModeCli": "仅本地 CLI",
|
|
674
|
+
"copilotModeWeb": "仅直连 API",
|
|
675
|
+
"copilotCliPath": "自定义 Copilot CLI 路径",
|
|
676
|
+
"copilotCliPathHint": "可选的 copilot 或 github-copilot-cli 二进制文件路径。",
|
|
663
677
|
"ollamaFallback": "Ollama 本地模型自动兜底",
|
|
664
678
|
"ollamaFallbackHint": "当云端配额耗尽或服务商故障时,自动将请求转发给本地 Ollama 实例。",
|
|
665
679
|
"ollamaBaseUrl": "Ollama 服务基础 URL",
|
|
@@ -1904,6 +1918,32 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1904
1918
|
),
|
|
1905
1919
|
React.createElement('div', { className: 'dsub-sub' },
|
|
1906
1920
|
t('codexVerbosityHint')),
|
|
1921
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1922
|
+
React.createElement('span', { className: 'dsub-h' }, t('copilotMode')),
|
|
1923
|
+
React.createElement('select', {
|
|
1924
|
+
className: 'dsub-select',
|
|
1925
|
+
value: draft.copilotMode || 'auto',
|
|
1926
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { copilotMode: e.target.value })),
|
|
1927
|
+
},
|
|
1928
|
+
React.createElement('option', { value: 'auto' }, t('copilotModeAuto')),
|
|
1929
|
+
React.createElement('option', { value: 'cli' }, t('copilotModeCli')),
|
|
1930
|
+
React.createElement('option', { value: 'web' }, t('copilotModeWeb')),
|
|
1931
|
+
),
|
|
1932
|
+
),
|
|
1933
|
+
React.createElement('div', { className: 'dsub-sub' },
|
|
1934
|
+
t('copilotModeHint')),
|
|
1935
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
1936
|
+
React.createElement('span', { className: 'dsub-h' }, t('copilotCliPath')),
|
|
1937
|
+
React.createElement('input', {
|
|
1938
|
+
className: 'dsub-input',
|
|
1939
|
+
type: 'text',
|
|
1940
|
+
value: draft.copilotCliPath || '',
|
|
1941
|
+
placeholder: 'copilot',
|
|
1942
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { copilotCliPath: e.target.value })),
|
|
1943
|
+
}),
|
|
1944
|
+
),
|
|
1945
|
+
React.createElement('div', { className: 'dsub-sub' },
|
|
1946
|
+
t('copilotCliPathHint')),
|
|
1907
1947
|
),
|
|
1908
1948
|
React.createElement('details', { className: 'dsub-group', open: true },
|
|
1909
1949
|
React.createElement('summary', { className: 'dsub-groupHead' }, t('groupFallback')),
|
package/lib/config-schema.js
CHANGED
|
@@ -96,6 +96,10 @@ export const Config = z.object({
|
|
|
96
96
|
.description('Custom model catalog for Codex.'),
|
|
97
97
|
claudeSystemPrefix: z.string().volatile().default('')
|
|
98
98
|
.description('System prompt prefix for Claude.'),
|
|
99
|
+
copilotMode: z.string().volatile().default('auto')
|
|
100
|
+
.description('#508 Execution mode for GitHub Copilot: auto (prefer CLI if available), cli (require copilot CLI), or web (direct web/OAuth API).'),
|
|
101
|
+
copilotCliPath: z.string().volatile().default('')
|
|
102
|
+
.description('#508 Custom binary path for copilot-cli (empty detects in PATH and ~/.npm-global).'),
|
|
99
103
|
claudeCcVersion: z.string().volatile().default('')
|
|
100
104
|
.description('Claude Code version header for billing attribution (default: 2.1.289.7e2).'),
|
|
101
105
|
claudeModels: z.array(z.any()).volatile().default([])
|
package/lib/diagnostics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { copilotCliInfo } from './vendors/copilot.js'
|
|
1
2
|
import { getSessionReasoningStats } from './reasoning-effort.js'
|
|
2
3
|
import { defaultConcurrencyQueue } from './concurrency-queue.js'
|
|
3
4
|
import { calculateCacheMetrics } from './analytics.js'
|
|
@@ -54,6 +55,7 @@ export async function createDiagnosticsReport({ live, store, history, NS, pkgVer
|
|
|
54
55
|
queue: defaultConcurrencyQueue.stats(),
|
|
55
56
|
pins: getSessionPinStats(),
|
|
56
57
|
reasoning: getSessionReasoningStats(),
|
|
58
|
+
copilotCli: copilotCliInfo(cfg),
|
|
57
59
|
decisions: {
|
|
58
60
|
recent: rows.filter((r) => r && r.decisionTrace).slice(0, 10).map((r) => ({
|
|
59
61
|
ts: r.ts,
|
package/lib/import-auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { readClaudeCredentials } from './vendors/claude-cli.js'
|
|
6
|
+
import { readCopilotCredentials } from './vendors/copilot-cli.js'
|
|
6
7
|
import { normalizeExpiresAt } from './blob.js'
|
|
7
8
|
import { isGoogleClientId, extractGoogleClientIdFromJwt, emailFromToken } from './jwt.js'
|
|
8
9
|
import { DEFAULT_CLI_CLIENT_ID, DEFAULT_CLI_CLIENT_SECRET } from './vendors/antigravity.js'
|
|
@@ -264,6 +265,19 @@ async function discoverLocalCliSessionsInner() {
|
|
|
264
265
|
detected['claude-cli'] = claudeEntry
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
// 9. GitHub Copilot token and CLI credentials
|
|
269
|
+
const copilotCreds = await readCopilotCredentials(activeHome)
|
|
270
|
+
if (copilotCreds) {
|
|
271
|
+
const copilotEntry = {
|
|
272
|
+
provider: 'copilot',
|
|
273
|
+
source: copilotCreds.source || 'file',
|
|
274
|
+
path: copilotCreds.path || 'copilot-cli',
|
|
275
|
+
email: copilotCreds.email || 'GitHub Copilot User',
|
|
276
|
+
hasRefreshToken: !!copilotCreds.refreshToken,
|
|
277
|
+
}
|
|
278
|
+
detected.copilot = copilotEntry
|
|
279
|
+
detected['copilot-cli'] = copilotEntry
|
|
280
|
+
}
|
|
267
281
|
// 9. GitHub Copilot token
|
|
268
282
|
const copilotPaths = [
|
|
269
283
|
homeFile('.config', 'github-copilot', 'hosts.json'),
|
|
@@ -317,6 +331,17 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
317
331
|
}
|
|
318
332
|
|
|
319
333
|
if (canonical === 'copilot') {
|
|
334
|
+
const creds = await readCopilotCredentials(options && options.home)
|
|
335
|
+
if (creds && creds.accessToken) {
|
|
336
|
+
return {
|
|
337
|
+
accessToken: creds.accessToken,
|
|
338
|
+
refreshToken: creds.refreshToken || creds.accessToken,
|
|
339
|
+
githubToken: creds.githubToken || creds.accessToken,
|
|
340
|
+
expiresAt: creds.expiresAt || 0,
|
|
341
|
+
email: creds.email || 'GitHub Copilot User',
|
|
342
|
+
label: creds.label || creds.email || 'GitHub Copilot',
|
|
343
|
+
}
|
|
344
|
+
}
|
|
320
345
|
let tok = process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN || ''
|
|
321
346
|
let email = 'GitHub Copilot User'
|
|
322
347
|
if (info.source === 'file' && info.path) {
|
package/lib/routes/accounts.js
CHANGED
|
@@ -258,7 +258,7 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
258
258
|
body = null
|
|
259
259
|
}
|
|
260
260
|
const rawProv = body && body.provider
|
|
261
|
-
const prov = (rawProv === 'claude-cli') ? 'claude' : rawProv
|
|
261
|
+
const prov = (rawProv === 'claude-cli') ? 'claude' : (rawProv === 'copilot-cli') ? 'copilot' : rawProv
|
|
262
262
|
if (!prov || !isProvider(prov)) {
|
|
263
263
|
writeJson(res, 400, { ok: false, error: { code: 'bad_request', message: 'unknown or missing provider' } })
|
|
264
264
|
return
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// Native copilot-cli SDK mode with local tool calling (#508).
|
|
2
|
+
// Enforces 0 Cyrillic characters.
|
|
3
|
+
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { existsSync, accessSync, constants } from 'node:fs'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { join, delimiter } from 'node:path'
|
|
8
|
+
import { spawn } from 'node:child_process'
|
|
9
|
+
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Standard candidate binary names for GitHub Copilot CLI.
|
|
13
|
+
*/
|
|
14
|
+
export const COPILOT_CLI_BINARIES = [
|
|
15
|
+
'copilot',
|
|
16
|
+
'copilot.cmd',
|
|
17
|
+
'copilot.exe',
|
|
18
|
+
'github-copilot-cli',
|
|
19
|
+
'github-copilot-cli.cmd',
|
|
20
|
+
'copilot-cli',
|
|
21
|
+
'gh-copilot',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read GitHub Copilot credentials from local environments or config files.
|
|
26
|
+
* @param {string} [home] Test seam for home directory
|
|
27
|
+
* @param {NodeJS.ProcessEnv} [env] Test seam for process environment
|
|
28
|
+
* @returns {Promise<{ accessToken: string, githubToken: string, refreshToken?: string, expiresAt?: number, email?: string, label?: string, source: string, path?: string } | null>}
|
|
29
|
+
*/
|
|
30
|
+
export async function readCopilotCredentials(home, env = process.env) {
|
|
31
|
+
const activeHome = home || homedir()
|
|
32
|
+
|
|
33
|
+
// 1. Environment variables
|
|
34
|
+
const envToken = env.GITHUB_COPILOT_TOKEN || env.GH_TOKEN || env.GITHUB_TOKEN || env.COPILOT_API_KEY
|
|
35
|
+
if (envToken && typeof envToken === 'string' && envToken.trim()) {
|
|
36
|
+
const clean = envToken.trim()
|
|
37
|
+
return {
|
|
38
|
+
accessToken: clean,
|
|
39
|
+
githubToken: clean,
|
|
40
|
+
refreshToken: clean,
|
|
41
|
+
expiresAt: 0,
|
|
42
|
+
email: 'GitHub Copilot User',
|
|
43
|
+
label: 'copilot-env',
|
|
44
|
+
source: 'env',
|
|
45
|
+
path: env.GITHUB_COPILOT_TOKEN ? 'GITHUB_COPILOT_TOKEN' : (env.GH_TOKEN ? 'GH_TOKEN' : 'GITHUB_TOKEN'),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Known configuration files
|
|
50
|
+
const fileCandidates = [
|
|
51
|
+
join(activeHome, '.config', 'github-copilot', 'hosts.json'),
|
|
52
|
+
join(activeHome, '.config', 'github-copilot', 'apps.json'),
|
|
53
|
+
join(activeHome, '.copilot-cli', 'config.json'),
|
|
54
|
+
join(activeHome, '.config', 'gh', 'hosts.yml'),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for (const filePath of fileCandidates) {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(filePath)) continue
|
|
60
|
+
const content = await readFile(filePath, 'utf8')
|
|
61
|
+
if (filePath.endsWith('.json')) {
|
|
62
|
+
const json = JSON.parse(content)
|
|
63
|
+
if (!json) continue
|
|
64
|
+
const gh = json['github.com'] || json
|
|
65
|
+
const tok = gh.oauth_token || gh.token || gh.access_token || gh.accessToken
|
|
66
|
+
if (tok) {
|
|
67
|
+
return {
|
|
68
|
+
accessToken: String(tok),
|
|
69
|
+
githubToken: String(tok),
|
|
70
|
+
refreshToken: String(tok),
|
|
71
|
+
expiresAt: 0,
|
|
72
|
+
email: gh.user || 'GitHub Copilot User',
|
|
73
|
+
label: gh.user || 'copilot-cli',
|
|
74
|
+
source: 'file',
|
|
75
|
+
path: filePath,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) {
|
|
79
|
+
const lines = content.split('\n')
|
|
80
|
+
let inGithub = false
|
|
81
|
+
let user = ''
|
|
82
|
+
let oauthToken = ''
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const trimmed = line.trim()
|
|
85
|
+
if (/^github\.com:/.test(trimmed)) {
|
|
86
|
+
inGithub = true
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (inGithub) {
|
|
90
|
+
if (/^[a-zA-Z0-9.-]+:/.test(trimmed) && !trimmed.startsWith('oauth_token') && !trimmed.startsWith('user')) {
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
const userMatch = /user:\s*([^\s#]+)/.exec(trimmed)
|
|
94
|
+
if (userMatch) user = userMatch[1]
|
|
95
|
+
const tokenMatch = /oauth_token:\s*([^\s#]+)/.exec(trimmed)
|
|
96
|
+
if (tokenMatch) oauthToken = tokenMatch[1]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (oauthToken) {
|
|
100
|
+
return {
|
|
101
|
+
accessToken: oauthToken,
|
|
102
|
+
githubToken: oauthToken,
|
|
103
|
+
refreshToken: oauthToken,
|
|
104
|
+
expiresAt: 0,
|
|
105
|
+
email: user || 'GitHub Copilot User',
|
|
106
|
+
label: user || 'gh-hosts',
|
|
107
|
+
source: 'file',
|
|
108
|
+
path: filePath,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Continue to next candidate
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Locate Copilot CLI binary on system.
|
|
122
|
+
* @param {string} [customPath] Explicit user-configured path
|
|
123
|
+
* @param {NodeJS.ProcessEnv} [env] Process environment
|
|
124
|
+
* @param {string} [platform] Process platform
|
|
125
|
+
* @returns {string|null} Resolved executable path or null
|
|
126
|
+
*/
|
|
127
|
+
export function findCopilotCli(customPath, env = process.env, platform = process.platform) {
|
|
128
|
+
if (customPath && typeof customPath === 'string' && customPath.trim()) {
|
|
129
|
+
const p = customPath.trim()
|
|
130
|
+
try {
|
|
131
|
+
if (existsSync(p)) return p
|
|
132
|
+
} catch { /* best-effort */ }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pathVar = env.PATH || ''
|
|
136
|
+
const pathDirs = pathVar.split(delimiter).filter(Boolean)
|
|
137
|
+
|
|
138
|
+
const home = env.HOME || env.USERPROFILE || homedir()
|
|
139
|
+
if (home) {
|
|
140
|
+
pathDirs.push(
|
|
141
|
+
join(home, '.npm-global', 'bin'),
|
|
142
|
+
join(home, '.local', 'bin'),
|
|
143
|
+
join(home, 'AppData', 'Roaming', 'npm'),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const dir of pathDirs) {
|
|
148
|
+
for (const bin of COPILOT_CLI_BINARIES) {
|
|
149
|
+
const fullPath = join(dir, bin)
|
|
150
|
+
try {
|
|
151
|
+
if (existsSync(fullPath)) {
|
|
152
|
+
if (platform !== 'win32') {
|
|
153
|
+
try {
|
|
154
|
+
accessSync(fullPath, constants.X_OK)
|
|
155
|
+
} catch {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return fullPath
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Continue searching
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Verify Copilot CLI availability and return status.
|
|
172
|
+
* @param {{ customPath?: string, env?: NodeJS.ProcessEnv }} [options]
|
|
173
|
+
* @returns {{ available: boolean, path?: string, error?: string }}
|
|
174
|
+
*/
|
|
175
|
+
export function isCopilotCliAvailable(options = {}) {
|
|
176
|
+
const binary = findCopilotCli(options.customPath, options.env)
|
|
177
|
+
if (binary) {
|
|
178
|
+
return { available: true, path: binary }
|
|
179
|
+
}
|
|
180
|
+
return { available: false, error: 'copilot_binary_not_found' }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Translate DSH turn options to Copilot CLI arguments and environment.
|
|
185
|
+
* @param {any} options
|
|
186
|
+
* @param {any} [blob]
|
|
187
|
+
* @param {string} [binaryPath]
|
|
188
|
+
* @returns {{ args: string[], env: NodeJS.ProcessEnv, inputPayload: string }}
|
|
189
|
+
*/
|
|
190
|
+
export function prepareCopilotCliInvocation(options, blob = {}, _binaryPath = 'copilot') {
|
|
191
|
+
const model = options.model || 'claude-3.7-sonnet'
|
|
192
|
+
const token = blob.accessToken || blob.githubToken || blob.refreshToken || ''
|
|
193
|
+
|
|
194
|
+
const args = [
|
|
195
|
+
'--model', model,
|
|
196
|
+
'--stream',
|
|
197
|
+
'--json',
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
if (options.temperature != null) {
|
|
201
|
+
args.push('--temperature', String(options.temperature))
|
|
202
|
+
}
|
|
203
|
+
if (options.maxTokens != null) {
|
|
204
|
+
args.push('--max-tokens', String(options.maxTokens))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(options.tools) && options.tools.length) {
|
|
208
|
+
args.push('--tools', JSON.stringify(options.tools))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const env = {
|
|
212
|
+
...process.env,
|
|
213
|
+
...(token ? {
|
|
214
|
+
GITHUB_TOKEN: token,
|
|
215
|
+
GH_TOKEN: token,
|
|
216
|
+
GITHUB_COPILOT_TOKEN: token,
|
|
217
|
+
COPILOT_API_KEY: token,
|
|
218
|
+
} : {}),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const inputPayload = JSON.stringify({
|
|
222
|
+
model,
|
|
223
|
+
messages: options.messages || (options.prompt ? [{ role: 'user', content: options.prompt }] : []),
|
|
224
|
+
tools: options.tools || [],
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return { args, env, inputPayload }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Stream execution from local Copilot CLI process.
|
|
232
|
+
* Yields canonical DSH events: block-start, text-delta, tool-call-delta, finish, usage.
|
|
233
|
+
* @param {object} params
|
|
234
|
+
* @param {string} params.binaryPath
|
|
235
|
+
* @param {any} params.options
|
|
236
|
+
* @param {any} [params.blob]
|
|
237
|
+
* @param {any} [params.config]
|
|
238
|
+
* @param {AbortSignal} [params.signal]
|
|
239
|
+
* @param {Function} [params.spawnImpl] Test seam for child_process.spawn
|
|
240
|
+
* @returns {AsyncGenerator<any, void, unknown>}
|
|
241
|
+
*/
|
|
242
|
+
export async function* streamCopilotCli({ binaryPath, options, blob, _config, signal, spawnImpl }) {
|
|
243
|
+
if (signal && signal.aborted) {
|
|
244
|
+
throw new LlmError('Request aborted by client', 'ABORTED')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { args, env, inputPayload } = prepareCopilotCliInvocation(options, blob, binaryPath)
|
|
248
|
+
const spawner = spawnImpl || spawn
|
|
249
|
+
|
|
250
|
+
let child
|
|
251
|
+
try {
|
|
252
|
+
child = spawner(binaryPath, args, {
|
|
253
|
+
env,
|
|
254
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
255
|
+
})
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new LlmError(`Failed to spawn Copilot CLI: ${err && err.message ? err.message : err}`, 'INTERNAL_ERROR')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (signal) {
|
|
261
|
+
const abortHandler = () => {
|
|
262
|
+
try { child.kill('SIGTERM') } catch { /* best-effort */ }
|
|
263
|
+
}
|
|
264
|
+
signal.addEventListener('abort', abortHandler, { once: true })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (child.stdin) {
|
|
268
|
+
child.stdin.write(inputPayload)
|
|
269
|
+
child.stdin.end()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let textStarted = false
|
|
273
|
+
let toolIndex = 0
|
|
274
|
+
let sawTools = false
|
|
275
|
+
const toolCallMap = new Map()
|
|
276
|
+
|
|
277
|
+
let stdoutBuffer = ''
|
|
278
|
+
let stderrBuffer = ''
|
|
279
|
+
|
|
280
|
+
if (child.stderr) {
|
|
281
|
+
child.stderr.on('data', (d) => { stderrBuffer += d.toString() })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const chunkQueue = []
|
|
285
|
+
let resolveNext = null
|
|
286
|
+
let processEnded = false
|
|
287
|
+
let processError = null
|
|
288
|
+
|
|
289
|
+
child.stdout.on('data', (chunk) => {
|
|
290
|
+
stdoutBuffer += chunk.toString()
|
|
291
|
+
let newlineIndex
|
|
292
|
+
while ((newlineIndex = stdoutBuffer.indexOf('\n')) >= 0) {
|
|
293
|
+
const line = stdoutBuffer.slice(0, newlineIndex).trim()
|
|
294
|
+
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1)
|
|
295
|
+
if (line) {
|
|
296
|
+
chunkQueue.push(line)
|
|
297
|
+
if (resolveNext) {
|
|
298
|
+
const r = resolveNext
|
|
299
|
+
resolveNext = null
|
|
300
|
+
r()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
child.on('error', (err) => {
|
|
307
|
+
processError = err
|
|
308
|
+
if (resolveNext) {
|
|
309
|
+
const r = resolveNext
|
|
310
|
+
resolveNext = null
|
|
311
|
+
r()
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
child.on('close', (code) => {
|
|
316
|
+
processEnded = true
|
|
317
|
+
if (stdoutBuffer.trim()) {
|
|
318
|
+
chunkQueue.push(stdoutBuffer.trim())
|
|
319
|
+
stdoutBuffer = ''
|
|
320
|
+
}
|
|
321
|
+
if (code !== 0 && !chunkQueue.length) {
|
|
322
|
+
processError = new LlmError(`Copilot CLI exited with code ${code}: ${stderrBuffer.trim() || 'unknown error'}`, 'VENDOR')
|
|
323
|
+
}
|
|
324
|
+
if (resolveNext) {
|
|
325
|
+
const r = resolveNext
|
|
326
|
+
resolveNext = null
|
|
327
|
+
r()
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
while (true) {
|
|
332
|
+
if (chunkQueue.length) {
|
|
333
|
+
const line = chunkQueue.shift()
|
|
334
|
+
const raw = line.startsWith('data:') ? line.slice(5).trim() : line
|
|
335
|
+
if (raw === '[DONE]') break
|
|
336
|
+
|
|
337
|
+
let parsed = null
|
|
338
|
+
try { parsed = JSON.parse(raw) } catch { /* text fragment */ }
|
|
339
|
+
|
|
340
|
+
if (parsed) {
|
|
341
|
+
// 1. Text delta
|
|
342
|
+
const text = parsed.text || parsed.delta?.content || parsed.content || (parsed.choices && parsed.choices[0]?.delta?.content)
|
|
343
|
+
if (typeof text === 'string' && text) {
|
|
344
|
+
if (!textStarted) {
|
|
345
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
346
|
+
textStarted = true
|
|
347
|
+
}
|
|
348
|
+
yield { type: 'text-delta', index: 0, text }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. Tool calls
|
|
352
|
+
const toolCalls = parsed.tool_calls || parsed.delta?.tool_calls || (parsed.choices && parsed.choices[0]?.delta?.tool_calls) || (parsed.tool_call ? [parsed.tool_call] : null)
|
|
353
|
+
if (Array.isArray(toolCalls)) {
|
|
354
|
+
sawTools = true
|
|
355
|
+
for (const tc of toolCalls) {
|
|
356
|
+
const id = tc.id || `call_${toolIndex}`
|
|
357
|
+
const name = tc.name || (tc.function && tc.function.name)
|
|
358
|
+
const argsDelta = tc.argumentsDelta || tc.arguments || (tc.function && tc.function.arguments) || ''
|
|
359
|
+
let idx = toolCallMap.get(id)
|
|
360
|
+
if (idx === undefined) {
|
|
361
|
+
idx = ++toolIndex
|
|
362
|
+
toolCallMap.set(id, idx)
|
|
363
|
+
yield { type: 'block-start', index: idx, blockType: 'tool-call' }
|
|
364
|
+
}
|
|
365
|
+
yield {
|
|
366
|
+
type: 'tool-call-delta',
|
|
367
|
+
index: idx,
|
|
368
|
+
id,
|
|
369
|
+
...(name ? { name } : {}),
|
|
370
|
+
argumentsDelta: typeof argsDelta === 'string' ? argsDelta : JSON.stringify(argsDelta),
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 3. Usage
|
|
376
|
+
if (parsed.usage) {
|
|
377
|
+
yield { type: 'usage', usage: parsed.usage }
|
|
378
|
+
}
|
|
379
|
+
} else if (raw) {
|
|
380
|
+
// Plain text stdout line
|
|
381
|
+
if (!textStarted) {
|
|
382
|
+
yield { type: 'block-start', index: 0, blockType: 'text' }
|
|
383
|
+
textStarted = true
|
|
384
|
+
}
|
|
385
|
+
yield { type: 'text-delta', index: 0, text: raw }
|
|
386
|
+
}
|
|
387
|
+
} else if (processEnded) {
|
|
388
|
+
if (processError) throw processError
|
|
389
|
+
break
|
|
390
|
+
} else {
|
|
391
|
+
await new Promise((res) => { resolveNext = res })
|
|
392
|
+
if (processError && !chunkQueue.length) throw processError
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
yield {
|
|
397
|
+
type: 'finish',
|
|
398
|
+
reason: { kind: sawTools ? 'tool-calls' : 'stop' },
|
|
399
|
+
}
|
|
400
|
+
}
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -2,6 +2,7 @@ import { LlmError } from "@deepseek-ai/dsh-llm"
|
|
|
2
2
|
import { openaiMessages, openaiTools, modelCatalog } from "../messages.js"
|
|
3
3
|
import { fetchWithTimeout } from "../http.js"
|
|
4
4
|
import { openaiChatStream, readJson, httpError } from "../wire.js"
|
|
5
|
+
import { findCopilotCli, streamCopilotCli, isCopilotCliAvailable, COPILOT_CLI_BINARIES } from "./copilot-cli.js"
|
|
5
6
|
|
|
6
7
|
export const id = "copilot"
|
|
7
8
|
|
|
@@ -53,6 +54,14 @@ const COPILOT_MODELS = [
|
|
|
53
54
|
}
|
|
54
55
|
]
|
|
55
56
|
|
|
57
|
+
export function copilotCliInfo(config = {}) {
|
|
58
|
+
const status = isCopilotCliAvailable({ customPath: config && config.copilotCliPath })
|
|
59
|
+
return {
|
|
60
|
+
...status,
|
|
61
|
+
supportedBinaries: COPILOT_CLI_BINARIES,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
export function providerInfo() {
|
|
57
66
|
return { id, name: "GitHub Copilot" }
|
|
58
67
|
}
|
|
@@ -280,7 +289,28 @@ export async function usage(blob, config, fetchImpl) {
|
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
291
|
|
|
283
|
-
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
|
292
|
+
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, spawnImpl }) {
|
|
293
|
+
const mode = (config && config.copilotMode) || (blob && blob.copilotMode) || 'auto'
|
|
294
|
+
const customPath = (config && config.copilotCliPath) || (blob && blob.copilotCliPath) || ''
|
|
295
|
+
|
|
296
|
+
if (mode !== 'web') {
|
|
297
|
+
const cliPath = findCopilotCli(customPath)
|
|
298
|
+
if (cliPath) {
|
|
299
|
+
try {
|
|
300
|
+
let yieldedAny = false
|
|
301
|
+
for await (const chunk of streamCopilotCli({ binaryPath: cliPath, options, blob, config, signal, spawnImpl })) {
|
|
302
|
+
yieldedAny = true
|
|
303
|
+
yield chunk
|
|
304
|
+
}
|
|
305
|
+
if (yieldedAny) return
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (mode === 'cli') throw err
|
|
308
|
+
// In auto mode, fallback seamlessly to web / OAuth execution
|
|
309
|
+
}
|
|
310
|
+
} else if (mode === 'cli') {
|
|
311
|
+
throw new LlmError('GitHub Copilot CLI binary not found on system', 'NOT_CONFIGURED')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
284
314
|
const impl = fetchImpl || fetch
|
|
285
315
|
let copilotToken = blob && blob.accessToken
|
|
286
316
|
const githubToken = blob && (blob.refreshToken || blob.githubToken)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.66",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|