@botconnector/bccli 0.5.0-rc.4 → 0.5.1
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/LICENSE +21 -203
- package/README.en.md +253 -0
- package/README.md +250 -0
- package/dist/abort.d.ts +5 -0
- package/dist/acp-HWHF25KU.js +221 -0
- package/dist/acp.d.ts +44 -0
- package/dist/agent.d.ts +158 -0
- package/dist/agents.d.ts +4 -0
- package/dist/args-XA77XUMD.js +13 -0
- package/dist/args.d.ts +59 -0
- package/dist/budget.d.ts +26 -0
- package/dist/checkpoints.d.ts +35 -0
- package/dist/chunk-2IP5F3EK.js +56 -0
- package/dist/chunk-5CVHTYBQ.js +270 -0
- package/dist/chunk-6LO6YRDY.js +216 -0
- package/dist/chunk-6PJRWKVI.js +50 -0
- package/dist/chunk-6YRTPXGY.js +8 -0
- package/dist/chunk-7OTFZK4B.js +159 -0
- package/dist/chunk-7U7CMKKI.js +861 -0
- package/dist/chunk-A3NBWIEQ.js +2706 -0
- package/dist/chunk-FY543EQW.js +50 -0
- package/dist/chunk-IDZLHEVL.js +524 -0
- package/dist/chunk-IF5RNXBI.js +341 -0
- package/dist/chunk-M5T4AM4I.js +256 -0
- package/dist/chunk-NGFWBVHD.js +71 -0
- package/dist/chunk-OCEQ2TRJ.js +1075 -0
- package/dist/chunk-OK2USAXX.js +88 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-RLSQAZBT.js +78 -0
- package/dist/chunk-VPFCIUC6.js +91 -0
- package/dist/chunk-YGBTNDPK.js +202 -0
- package/dist/chunk-YYTFJUO2.js +148 -0
- package/dist/chunk-ZXWRLN7E.js +378 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +251 -0
- package/dist/clipboardImage.d.ts +17 -0
- package/dist/commands.d.ts +16 -0
- package/dist/completion-IRVEVW6H.js +125 -0
- package/dist/completion.d.ts +14 -0
- package/dist/config-OVWCUVY7.js +25 -0
- package/dist/config-S3CZUIBB.js +34 -0
- package/dist/config.d.ts +99 -0
- package/dist/context.d.ts +13 -0
- package/dist/contextWindow.d.ts +3 -0
- package/dist/diff.d.ts +7 -0
- package/dist/exit.d.ts +9 -0
- package/dist/extensions.d.ts +34 -0
- package/dist/ghBotCli-FYKJDJOD.js +680 -0
- package/dist/ghBotCli.d.ts +9 -0
- package/dist/ghBotDispatch.d.ts +68 -0
- package/dist/ghBotRunner.d.ts +82 -0
- package/dist/ghBotServe.d.ts +21 -0
- package/dist/ghBotWebhook.d.ts +45 -0
- package/dist/hooks.d.ts +27 -0
- package/dist/i18n/id.d.ts +1 -0
- package/dist/i18n.d.ts +16 -0
- package/dist/integrationCli-GY73S2K7.js +876 -0
- package/dist/integrationCli.d.ts +9 -0
- package/dist/keybinds.d.ts +22 -0
- package/dist/login-KSN63EM4.js +18 -0
- package/dist/login.d.ts +21 -0
- package/dist/lsp.d.ts +73 -0
- package/dist/mcp/catalog.d.ts +14 -0
- package/dist/mcp/config.d.ts +36 -0
- package/dist/mcp/manager.d.ts +52 -0
- package/dist/mcp/oauth.d.ts +82 -0
- package/dist/mcpCli-VWSCCDWT.js +113 -0
- package/dist/mcpCli.d.ts +3 -0
- package/dist/mcpServer/serve.d.ts +8 -0
- package/dist/mcpServer/server.d.ts +19 -0
- package/dist/models.d.ts +13 -0
- package/dist/modelsCli-4Q6KFWV2.js +36 -0
- package/dist/modelsCli.d.ts +15 -0
- package/dist/npmRunner-LU43GM3W.js +46 -0
- package/dist/npmRunner.d.ts +21 -0
- package/dist/oauthLogin-JWXQZTMZ.js +210 -0
- package/dist/oauthLogin.d.ts +47 -0
- package/dist/onboarding.d.ts +6 -0
- package/dist/permissions.d.ts +30 -0
- package/dist/pluginSpec.d.ts +25 -0
- package/dist/plugins.d.ts +45 -0
- package/dist/pluginsCli-7XWDLJEL.js +288 -0
- package/dist/pluginsCli.d.ts +16 -0
- package/dist/prCli-FH7N26D4.js +94 -0
- package/dist/prCli.d.ts +46 -0
- package/dist/presets.d.ts +8 -0
- package/dist/print.d.ts +17 -0
- package/dist/provider.d.ts +145 -0
- package/dist/providerCli-YMG4G56R.js +15 -0
- package/dist/providerCli.d.ts +17 -0
- package/dist/providers.d.ts +6 -0
- package/dist/reasoning.d.ts +12 -0
- package/dist/repomap.d.ts +10 -0
- package/dist/sdk.d.ts +61 -0
- package/dist/sdk.js +6532 -0
- package/dist/serve-HSFF6MGB.js +178 -0
- package/dist/serve-R2IHP26A.js +532 -0
- package/dist/serve.d.ts +37 -0
- package/dist/serveHeartbeat.d.ts +14 -0
- package/dist/serveProtocol.d.ts +47 -0
- package/dist/session-YM36TGU7.js +12 -0
- package/dist/session.d.ts +33 -0
- package/dist/setup-3YB6EKYX.js +24 -0
- package/dist/setup.d.ts +79 -0
- package/dist/slash/copy.d.ts +1 -0
- package/dist/slash/diff.d.ts +1 -0
- package/dist/slash/doctor.d.ts +18 -0
- package/dist/slash/export.d.ts +2 -0
- package/dist/slash/info.d.ts +46 -0
- package/dist/slash/memory.d.ts +10 -0
- package/dist/statsCli-I4TSDU3X.js +353 -0
- package/dist/statsCli.d.ts +62 -0
- package/dist/textToolCalls.d.ts +6 -0
- package/dist/thinking.d.ts +17 -0
- package/dist/tokenBudget.d.ts +22 -0
- package/dist/tools/bash.d.ts +54 -0
- package/dist/tools/diagnostics.d.ts +13 -0
- package/dist/tools/edit.d.ts +12 -0
- package/dist/tools/fetch.d.ts +41 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/globSafety.d.ts +7 -0
- package/dist/tools/grep.d.ts +25 -0
- package/dist/tools/ignore.d.ts +2 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/paths.d.ts +4 -0
- package/dist/tools/plan.d.ts +10 -0
- package/dist/tools/read.d.ts +16 -0
- package/dist/tools/repomap.d.ts +2 -0
- package/dist/tools/sandbox.d.ts +15 -0
- package/dist/tools/shell.d.ts +18 -0
- package/dist/tools/skill.d.ts +3 -0
- package/dist/tools/task.d.ts +28 -0
- package/dist/tools/todo.d.ts +13 -0
- package/dist/tools/types.d.ts +52 -0
- package/dist/tools/websearch.d.ts +9 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/transferCli-3B4LMOYI.js +73 -0
- package/dist/transferCli.d.ts +23 -0
- package/dist/ui/App.d.ts +7 -0
- package/dist/ui/DiffView.d.ts +3 -0
- package/dist/ui/EditableText.d.ts +7 -0
- package/dist/ui/LinePrompt.d.ts +6 -0
- package/dist/ui/ListPicker.d.ts +11 -0
- package/dist/ui/Markdown.d.ts +8 -0
- package/dist/ui/McpMenu.d.ts +11 -0
- package/dist/ui/ModelPicker.d.ts +6 -0
- package/dist/ui/PermissionPrompt.d.ts +5 -0
- package/dist/ui/PlanApproval.d.ts +5 -0
- package/dist/ui/PromptInput.d.ts +20 -0
- package/dist/ui/ProviderMenu.d.ts +10 -0
- package/dist/ui/Spinner.d.ts +4 -0
- package/dist/ui/StatusBar.d.ts +14 -0
- package/dist/ui/TodoList.d.ts +4 -0
- package/dist/ui/ToolBlock.d.ts +14 -0
- package/dist/ui/complete.d.ts +8 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/lineEdit.d.ts +42 -0
- package/dist/ui/liveCap.d.ts +29 -0
- package/dist/ui/markdownLayout.d.ts +23 -0
- package/dist/ui/replay.d.ts +4 -0
- package/dist/ui/stdinFilter.d.ts +18 -0
- package/dist/ui/table.d.ts +20 -0
- package/dist/ui/theme.d.ts +5 -0
- package/dist/ui/thinkingMouse.d.ts +38 -0
- package/dist/ui/transcript.d.ts +55 -0
- package/dist/ui/tty.d.ts +11 -0
- package/dist/ui/vim.d.ts +26 -0
- package/dist/ui-TAZMRXRI.js +3044 -0
- package/dist/update-XC3B6SJC.js +145 -0
- package/dist/update.d.ts +30 -0
- package/dist/verify.d.ts +16 -0
- package/dist/version.d.ts +1 -0
- package/dist/webBudget.d.ts +25 -0
- package/dist/worktree.d.ts +12 -0
- package/package.json +59 -35
- package/THIRD_PARTY_NOTICES.md +0 -9
- package/branding.mjs +0 -347
- package/cli.mjs +0 -120
- package/core.mjs +0 -260
- package/credentials.mjs +0 -52
- package/gateway.mjs +0 -2
- package/model-command.mjs +0 -61
- package/provider-cli.mjs +0 -77
- package/providers.mjs +0 -166
- package/proxy.mjs +0 -242
- package/startup-menu.mjs +0 -120
package/core.mjs
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
// BCCLI candidate adapter. Intentionally outside the Qwen Code engine.
|
|
2
|
-
import { spawn } from 'node:child_process'
|
|
3
|
-
import { createRequire } from 'node:module'
|
|
4
|
-
import { homedir } from 'node:os'
|
|
5
|
-
import { readFileSync, realpathSync, lstatSync } from 'node:fs'
|
|
6
|
-
import { dirname, join, isAbsolute } from 'node:path'
|
|
7
|
-
import { startGatewayProxy } from './proxy.mjs'
|
|
8
|
-
import { GATEWAY } from './gateway.mjs'
|
|
9
|
-
|
|
10
|
-
export { GATEWAY } from './gateway.mjs'
|
|
11
|
-
export const DEFAULT_MODEL = 'ling-3.0-flash'
|
|
12
|
-
|
|
13
|
-
// Qwen Code 0.26.3 Windows tool-loop exits cleanly with Node 24.22.1
|
|
14
|
-
// (GitHub Actions), but repeatedly crashes on Windows Node 24.18.0
|
|
15
|
-
// with libuv UV_HANDLE_CLOSING after delivering the tool result.
|
|
16
|
-
// This guard affects ONLY the isolated candidate, not public BCCLI.
|
|
17
|
-
export function assertCandidateRuntimeSupported({ platform = process.platform, nodeVersion = process.versions.node } = {}) {
|
|
18
|
-
const parts = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/.exec(String(nodeVersion))
|
|
19
|
-
if (!parts) throw Error('Cannot verify the Node.js version for BCCLI candidate.')
|
|
20
|
-
const major = Number(parts[1]), minor = Number(parts[2]), patch = Number(parts[3])
|
|
21
|
-
if (platform === 'win32' && major === 24 && (minor < 22 || (minor === 22 && patch < 1))) {
|
|
22
|
-
throw Error('Windows Node.js ' + nodeVersion + ' may crash after coding tools. Use Node.js 24.22.1 or newer, or Node.js 22.23.4. Existing public BCCLI is unaffected.')
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
const MODEL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/
|
|
26
|
-
const ALLOWED = new Set(['models', 'run', 'chat', 'resume', 'auth', 'provider'])
|
|
27
|
-
// Public BCCLI commands are not supported by this isolated candidate. Do
|
|
28
|
-
// not accidentally treat them as AI prompts, which could spend user credits.
|
|
29
|
-
const PUBLIC_ONLY_COMMANDS = new Set([
|
|
30
|
-
'login', 'logout', 'mcp', 'connect', 'disconnect',
|
|
31
|
-
'integrations', 'update', 'serve', 'acp', 'plugins', 'gh-bot',
|
|
32
|
-
'completion', 'stats', 'export', 'import', 'pr',
|
|
33
|
-
])
|
|
34
|
-
|
|
35
|
-
export function parseArgs(argv) {
|
|
36
|
-
const args = [...argv]
|
|
37
|
-
// Public BCCLI accepts options before subcommands. This candidate does
|
|
38
|
-
// not: reject those spellings rather than accidentally charging for a
|
|
39
|
-
// prompt such as "-m ling login" or "-m ling models".
|
|
40
|
-
if (!args.includes('-p') && !args.includes('--print') && !args.includes('-c') && !args.includes('--continue') && !ALLOWED.has(args[0])) {
|
|
41
|
-
let firstWord
|
|
42
|
-
for (let i = 0; i < args.length; i++) {
|
|
43
|
-
const flag = args[i]
|
|
44
|
-
if (flag === '--model' || flag === '-m' || flag === '--mcp-config') { i++; continue }
|
|
45
|
-
if (flag.startsWith('-')) continue
|
|
46
|
-
firstWord = flag
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
if (firstWord && (PUBLIC_ONLY_COMMANDS.has(firstWord) || ALLOWED.has(firstWord))) {
|
|
50
|
-
throw Error('Subcommand "' + firstWord + '" must come first in this candidate. No AI request was sent.')
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// Check old public subcommands BEFORE rewriting -p to run: otherwise
|
|
54
|
-
// "login -p" might accidentally become a billable model prompt.
|
|
55
|
-
if (args.length && PUBLIC_ONLY_COMMANDS.has(args[0])) {
|
|
56
|
-
throw Error('Command "' + args[0] + '" is available only in public BCCLI 0.4.17, not this isolated candidate. No AI request was sent.')
|
|
57
|
-
}
|
|
58
|
-
// Qwen handles both -c and -r natively. Never combine the two session modes.
|
|
59
|
-
if (args.some(a => a === '-c' || a === '--continue') && args.some(a => a === '-r' || a === '--resume')) {
|
|
60
|
-
throw Error('Use either -c or -r, not both. No AI request was sent.')
|
|
61
|
-
}
|
|
62
|
-
// The public -c/--continue resumes the last *current-project* session.
|
|
63
|
-
// Candidate Qwen 'resume' already implements that behavior in isolation.
|
|
64
|
-
const continueFlags = args.filter(a => a === '-c' || a === '--continue')
|
|
65
|
-
if (continueFlags.length > 1) throw Error('Specify -c / --continue only once.')
|
|
66
|
-
if (continueFlags.length) {
|
|
67
|
-
if (args.some(a => a === '-p' || a === '--print')) throw Error('Combining -c with -p is not yet supported by the candidate.')
|
|
68
|
-
const otherArgs = args.filter(a => a !== continueFlags[0])
|
|
69
|
-
if (ALLOWED.has(otherArgs[0])) throw Error('-c / --continue is not compatible with an explicit subcommand; use resume directly.')
|
|
70
|
-
args.splice(args.indexOf(continueFlags[0]), 1)
|
|
71
|
-
args.unshift('resume')
|
|
72
|
-
}
|
|
73
|
-
// Keep public BCCLI's common noninteractive invocation as a safe alias.
|
|
74
|
-
// Do not imply parity with public JSON/stream-json.
|
|
75
|
-
const printIndexes = args.flatMap((arg, i) => arg === '-p' || arg === '--print' ? [i] : [])
|
|
76
|
-
if (printIndexes.length > 1) throw Error('Specify -p / --print only once.')
|
|
77
|
-
if (printIndexes.length === 1) {
|
|
78
|
-
args.splice(printIndexes[0], 1)
|
|
79
|
-
if (ALLOWED.has(args[0]) && args[0] !== 'run') {
|
|
80
|
-
throw Error('-p / --print only supports a single headless run, not ' + args[0] + '.')
|
|
81
|
-
}
|
|
82
|
-
if (args[0] !== 'run') args.unshift('run')
|
|
83
|
-
}
|
|
84
|
-
const command = ALLOWED.has(args[0]) ? args.shift() : 'chat'
|
|
85
|
-
let model, mcpConfig, provider, json = false, resumeSession
|
|
86
|
-
const prompt = []
|
|
87
|
-
while (args.length) {
|
|
88
|
-
const arg = args.shift()
|
|
89
|
-
if (arg === '-r' || arg === '--resume') {
|
|
90
|
-
if (command !== 'chat' || resumeSession !== undefined) throw Error('Use -r [SESSION_UUID] only once for interactive chat.')
|
|
91
|
-
const next = args[0]
|
|
92
|
-
if (next && !next.startsWith('-')) {
|
|
93
|
-
if (!/^[a-f\d]{8}-(?:[a-f\d]{4}-){3}[a-f\d]{12}$/i.test(next)) {
|
|
94
|
-
throw Error('Session ID must be a UUID. No AI request was sent.')
|
|
95
|
-
}
|
|
96
|
-
resumeSession = args.shift()
|
|
97
|
-
} else {
|
|
98
|
-
resumeSession = null // Native Qwen --resume with no ID opens its own session picker.
|
|
99
|
-
}
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
if (arg === '--model' || arg === '-m') {
|
|
103
|
-
if (model !== undefined || !args.length) throw Error('Specify --model exactly once with an ID.')
|
|
104
|
-
model = args.shift()
|
|
105
|
-
continue
|
|
106
|
-
}
|
|
107
|
-
if (arg === '--provider') {
|
|
108
|
-
if (provider !== undefined || !args.length || args[0].startsWith('-')) throw Error('--provider requires exactly one provider ID.')
|
|
109
|
-
provider = args.shift()
|
|
110
|
-
continue
|
|
111
|
-
}
|
|
112
|
-
if (arg === '--mcp-config') {
|
|
113
|
-
if (mcpConfig !== undefined || !args.length || args[0].startsWith('-')) throw Error('--mcp-config requires exactly one absolute path.')
|
|
114
|
-
mcpConfig = args.shift()
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
if (arg === '--json' && command === 'models') { json = true; continue }
|
|
118
|
-
if (arg.startsWith('-')) throw Error('Unsupported option. POC intentionally blocks passthrough flags.')
|
|
119
|
-
prompt.push(arg)
|
|
120
|
-
}
|
|
121
|
-
if (command === 'provider' && (model || mcpConfig || provider || json)) throw Error('Use: provider list | add ID | use ID | remove ID.')
|
|
122
|
-
if (command === 'auth' && (model || mcpConfig || json || prompt.join(' ') !== 'status')) throw Error('Use: auth status (offline and read-only).')
|
|
123
|
-
if (command === 'models' && (model || mcpConfig || prompt.length)) throw Error('Use: models [--json].')
|
|
124
|
-
if (resumeSession !== undefined && prompt.length) throw Error('Session picker cannot be combined with a prompt.')
|
|
125
|
-
if (command === 'run' && !prompt.length) throw Error('Use: run --model MODEL "prompt".')
|
|
126
|
-
return { command, model, json, prompt: prompt.join(' '), ...(mcpConfig ? { mcpConfig } : {}), ...(provider ? { provider } : {}), ...(resumeSession !== undefined ? { resumeSession } : {}) }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export async function discoverModels({ key, fetchImpl = fetch, endpoint = GATEWAY }) {
|
|
130
|
-
if (!key || !key.trim()) throw Error('BOTCONNECTOR_API_KEY is required (do not supply it as a CLI argument).')
|
|
131
|
-
let response
|
|
132
|
-
try {
|
|
133
|
-
response = await fetchImpl(endpoint + '/models', {
|
|
134
|
-
method: 'GET',
|
|
135
|
-
headers: { authorization: 'Bearer ' + key, accept: 'application/json' },
|
|
136
|
-
redirect: 'error',
|
|
137
|
-
signal: AbortSignal.timeout(12000),
|
|
138
|
-
})
|
|
139
|
-
} catch {
|
|
140
|
-
throw Error('Gateway model discovery failed; no fallback provider was used.')
|
|
141
|
-
}
|
|
142
|
-
if (!response.ok) throw Error('Gateway model discovery failed (HTTP ' + response.status + ').')
|
|
143
|
-
let payload
|
|
144
|
-
try { payload = await response.json() }
|
|
145
|
-
catch { throw Error('Gateway model discovery returned invalid JSON.') }
|
|
146
|
-
if (!Array.isArray(payload?.data)) throw Error('Gateway model discovery returned no model list.')
|
|
147
|
-
const models = new Map()
|
|
148
|
-
for (const item of payload.data) {
|
|
149
|
-
if (typeof item?.id === 'string' && MODEL_ID.test(item.id)) {
|
|
150
|
-
models.set(item.id, { id: item.id, name: typeof item.name === 'string' ? item.name : item.id })
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (!models.size) throw Error('No valid models available to this key.')
|
|
154
|
-
return [...models.values()]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function selectModel(models, value) {
|
|
158
|
-
if (!value) throw Error('Select a model with --model ID or BOTCONNECTOR_MODEL_ID; run models first.')
|
|
159
|
-
if (!MODEL_ID.test(value)) throw Error('Invalid model ID.')
|
|
160
|
-
if (!models.some((item) => item.id === value)) throw Error('Model is not available for this BotConnector API key.')
|
|
161
|
-
return value
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function qwenArguments(model, prompt, endpoint = GATEWAY, { mcpConfig, continueSession = false, resumeSession } = {}) {
|
|
165
|
-
if (continueSession && resumeSession !== undefined) throw Error('Cannot combine native Qwen --continue with --resume.')
|
|
166
|
-
// --bare is security critical: project settings may otherwise override
|
|
167
|
-
// the provider baseUrl, forwarding an API key to an attacker-controlled endpoint.
|
|
168
|
-
const args = [
|
|
169
|
-
'--bare', '--auth-type', 'openai',
|
|
170
|
-
'--openai-base-url', endpoint,
|
|
171
|
-
'--model', model, '--approval-mode', 'default',
|
|
172
|
-
'--telemetry=false',
|
|
173
|
-
]
|
|
174
|
-
// The active AI can change with /model. Do not embed the STARTUP model
|
|
175
|
-
// here: doing so would falsely identify a later selected model.
|
|
176
|
-
// The BotConnector local proxy adds the actual request model to each
|
|
177
|
-
// chat call, without forcing the AI to impersonate BCCLI.
|
|
178
|
-
args.push('--append-system-prompt',
|
|
179
|
-
'Client application: BCCLI. The upstream coding engine is Qwen Code, which is software, not the AI model answering the user. Identify the responding AI using active_model_id from current-request runtime metadata (the model can change mid-session). If no current model ID is available, state that it is unknown. Do not call the assistant Qwen Code or BCCLI as if either were the AI model, and do not infer who developed the active model from the application or engine brand.')
|
|
180
|
-
if (mcpConfig) args.push('--mcp-config', mcpConfig)
|
|
181
|
-
if (continueSession) args.push('--continue')
|
|
182
|
-
if (resumeSession !== undefined) args.push('--resume', ...(resumeSession === null ? [] : [resumeSession]))
|
|
183
|
-
if (prompt) args.push(prompt)
|
|
184
|
-
return args
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function childEnvironment(base, key, model, { endpoint = GATEWAY, home, providerId = 'botconnector' } = {}) {
|
|
188
|
-
const env = { ...base }
|
|
189
|
-
// Prevent inherited cross-provider auth from becoming another route or key.
|
|
190
|
-
for (const name of [
|
|
191
|
-
'QWEN_OAUTH', 'BOTCONNECTOR_API_KEY', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL', 'QWEN_MODEL',
|
|
192
|
-
'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'GEMINI_API_KEY',
|
|
193
|
-
'GOOGLE_API_KEY', 'OPENROUTER_API_KEY', 'QWENCLOUD_API_KEY',
|
|
194
|
-
'QWENCLOUD_TOKEN_PLAN_API_KEY', 'DEEPSEEK_API_KEY', 'GROQ_API_KEY', 'XAI_API_KEY',
|
|
195
|
-
]) delete env[name]
|
|
196
|
-
env.OPENAI_API_KEY = key
|
|
197
|
-
env.OPENAI_BASE_URL = endpoint
|
|
198
|
-
env.OPENAI_MODEL = model
|
|
199
|
-
env.QWEN_MODEL = model
|
|
200
|
-
env.QWEN_HOME = home ?? (providerId === 'botconnector' ? join(homedir(), '.bccli-preview') : join(homedir(), '.bccli-preview', 'sessions-' + providerId))
|
|
201
|
-
env.BCCLI_ACTIVE_PROVIDER = providerId
|
|
202
|
-
env.CLI_TITLE = 'BCCLI - ' + process.cwd().split(/[\\/]/).filter(Boolean).at(-1)
|
|
203
|
-
env.QWEN_CODE_SKIP_UPDATE_CHECK_ONCE = 'true' // Never query or install upstream Qwen releases.
|
|
204
|
-
return env
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Explicit MCP servers run local commands on startup, so they must originate
|
|
208
|
-
// from a config file the user deliberately placed outside any cloned project.
|
|
209
|
-
// There is no automatic project/user MCP discovery when Qwen runs --bare.
|
|
210
|
-
export function resolveTrustedMcpConfig(input, { home = join(homedir(), '.bccli-preview') } = {}) {
|
|
211
|
-
if (!input) return undefined
|
|
212
|
-
if (!isAbsolute(input)) throw Error('MCP config must use an absolute path.')
|
|
213
|
-
let realHome, file, stats
|
|
214
|
-
try {
|
|
215
|
-
realHome = realpathSync(home)
|
|
216
|
-
file = realpathSync(input)
|
|
217
|
-
stats = lstatSync(input)
|
|
218
|
-
} catch {
|
|
219
|
-
throw Error('MCP config must be a readable file in the owner config directory.')
|
|
220
|
-
}
|
|
221
|
-
if (stats.isSymbolicLink() || !stats.isFile() || dirname(file) !== realHome) {
|
|
222
|
-
throw Error('MCP config must be a regular file directly in the owner config directory.')
|
|
223
|
-
}
|
|
224
|
-
let config
|
|
225
|
-
try { config = JSON.parse(readFileSync(file, 'utf8')) }
|
|
226
|
-
catch { throw Error('MCP config must contain valid JSON.') }
|
|
227
|
-
if (!config || typeof config !== 'object' || Array.isArray(config) ||
|
|
228
|
-
!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
|
229
|
-
throw Error('MCP config must contain mcpServers.')
|
|
230
|
-
}
|
|
231
|
-
if (Object.keys(config).some((name) => name !== 'mcpServers')) {
|
|
232
|
-
throw Error('MCP config may contain only mcpServers (no provider or credential overrides).')
|
|
233
|
-
}
|
|
234
|
-
if (!Object.keys(config.mcpServers).length) throw Error('MCP config must define at least one server.')
|
|
235
|
-
return file
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function resolveQwenEntrypoint() {
|
|
239
|
-
const require = createRequire(import.meta.url)
|
|
240
|
-
return join(dirname(require.resolve('@qwen-code/qwen-code/package.json')), 'cli-entry.js')
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export async function launchQwen({ key, model, prompt, mcpConfig, continueSession = false, resumeSession, upstream = GATEWAY, providerId = 'botconnector', env = process.env, entrypoint = resolveQwenEntrypoint(), spawnImpl = spawn, proxyFactory = startGatewayProxy }) {
|
|
244
|
-
// The real API key never enters Qwen, its tools, or any MCP child process.
|
|
245
|
-
// Only this launcher retains it while the ephemeral proxy is running.
|
|
246
|
-
const proxy = await proxyFactory({ key, endpoint: upstream })
|
|
247
|
-
try {
|
|
248
|
-
const args = [entrypoint, ...qwenArguments(model, prompt, proxy.baseUrl, { mcpConfig, continueSession, resumeSession })]
|
|
249
|
-
const child = spawnImpl(process.execPath, args, {
|
|
250
|
-
cwd: process.cwd(), env: childEnvironment(env, proxy.token, model, { endpoint: proxy.baseUrl, providerId }),
|
|
251
|
-
stdio: 'inherit', windowsHide: false, shell: false,
|
|
252
|
-
})
|
|
253
|
-
return await new Promise((resolve, reject) => {
|
|
254
|
-
child.once('error', reject)
|
|
255
|
-
child.once('exit', (code, signal) => resolve(code ?? (signal ? 130 : 1)))
|
|
256
|
-
})
|
|
257
|
-
} finally {
|
|
258
|
-
await proxy.close()
|
|
259
|
-
}
|
|
260
|
-
}
|
package/credentials.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// Credentials bridge: reuse an owner's existing BCCLI bc-cloud login without
|
|
2
|
-
// another provider account or a second credential store. Never log the key.
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
import { join, isAbsolute } from 'node:path'
|
|
5
|
-
import { lstatSync, readFileSync } from 'node:fs'
|
|
6
|
-
|
|
7
|
-
const MAX_CREDENTIAL_FILE_BYTES = 64 * 1024
|
|
8
|
-
|
|
9
|
-
export function botConnectorKey({ env = process.env, home = homedir() } = {}) {
|
|
10
|
-
if (typeof env.BOTCONNECTOR_API_KEY === 'string' && env.BOTCONNECTOR_API_KEY.trim()) {
|
|
11
|
-
return env.BOTCONNECTOR_API_KEY.trim()
|
|
12
|
-
}
|
|
13
|
-
const override = env.BCCLI_HOME
|
|
14
|
-
const base = override ? override : join(home, '.bccli')
|
|
15
|
-
if (!isAbsolute(base)) throw Error('BCCLI_HOME must be an absolute directory.')
|
|
16
|
-
const path = join(base, 'credentials')
|
|
17
|
-
let info
|
|
18
|
-
try { info = lstatSync(path) }
|
|
19
|
-
catch (error) {
|
|
20
|
-
if (error?.code === 'ENOENT') {
|
|
21
|
-
throw Error('No BotConnector login. Run bccli login to save your bc-cloud key, or set BOTCONNECTOR_API_KEY.')
|
|
22
|
-
}
|
|
23
|
-
throw Error('Cannot inspect the existing BCCLI credentials file.')
|
|
24
|
-
}
|
|
25
|
-
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_CREDENTIAL_FILE_BYTES) {
|
|
26
|
-
throw Error('Invalid BCCLI credentials file; do not follow symlinks or oversized files.')
|
|
27
|
-
}
|
|
28
|
-
let data
|
|
29
|
-
try { data = JSON.parse(readFileSync(path, 'utf8')) }
|
|
30
|
-
catch { throw Error('Existing BCCLI credentials are not valid JSON.') }
|
|
31
|
-
const key = data && typeof data === 'object' && !Array.isArray(data) ? data['bc-cloud'] : null
|
|
32
|
-
if (typeof key !== 'string' || !key.trim()) {
|
|
33
|
-
throw Error('No bc-cloud key saved. Run bccli login or set BOTCONNECTOR_API_KEY.')
|
|
34
|
-
}
|
|
35
|
-
return key.trim()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Offline, read-only status. Reuse the public BCCLI bc-cloud login; never
|
|
40
|
-
* duplicate, rewrite, mask-print, or migrate the long-lived secret.
|
|
41
|
-
* "configured" means a credential exists, not that Gateway accepted it.
|
|
42
|
-
*/
|
|
43
|
-
export function botConnectorCredentialStatus({ env = process.env, home = homedir() } = {}) {
|
|
44
|
-
const viaEnvironment = typeof env.BOTCONNECTOR_API_KEY === 'string' && !!env.BOTCONNECTOR_API_KEY.trim()
|
|
45
|
-
try {
|
|
46
|
-
botConnectorKey({ env, home })
|
|
47
|
-
return { configured: true, source: viaEnvironment ? 'environment' : 'public-bccli' }
|
|
48
|
-
} catch (error) {
|
|
49
|
-
// All botConnectorKey errors are intentionally static; do not include keys.
|
|
50
|
-
return { configured: false, source: 'none', reason: error instanceof Error ? error.message : 'Credential inspection failed.' }
|
|
51
|
-
}
|
|
52
|
-
}
|
package/gateway.mjs
DELETED
package/model-command.mjs
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// Generated, version-pinned Qwen slash command overlay for /model.
|
|
2
|
-
// Fetches the authenticated model catalog from the existing local BotConnector
|
|
3
|
-
// proxy on each invocation. Selection changes Qwen Config in the current
|
|
4
|
-
// session; it never opens upstream provider settings or writes project config.
|
|
5
|
-
export const BCCLI_MODEL_COMMAND = String.raw`var bccliModelLastList=[];var modelCommand={name:"model",get description(){return "Choose an AI model from the active provider"},argumentHint:"[number|model-id|search WORD|page N]",kind:"built-in",supportedModes:["interactive","non_interactive","acp"],action:__name(async(context,argumentText)=>{
|
|
6
|
-
var response=(type,content)=>({type:"message",messageType:type,content});
|
|
7
|
-
try{
|
|
8
|
-
var base=process.env.OPENAI_BASE_URL||"";
|
|
9
|
-
var key=process.env.OPENAI_API_KEY||"";
|
|
10
|
-
if(!/^http:\/\/127\.0\.0\.1:[0-9]{1,5}\/v1$/.test(base)||!key){
|
|
11
|
-
return response("error","BCCLI requires an authenticated local provider session.");
|
|
12
|
-
}
|
|
13
|
-
var result=await fetch(base+"/models",{method:"GET",headers:{Authorization:"Bearer "+key,Accept:"application/json"},redirect:"error",signal:AbortSignal.timeout(10000)});
|
|
14
|
-
if(!result.ok)return response("error","Provider model catalog is unavailable (HTTP "+result.status+"). No provider fallback.");
|
|
15
|
-
var payload=await result.json();
|
|
16
|
-
if(!Array.isArray(payload?.data))return response("error","Invalid provider model catalog.");
|
|
17
|
-
var catalog=new Map();
|
|
18
|
-
for(var entry of payload.data){
|
|
19
|
-
if(typeof entry?.id==="string" && /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/.test(entry.id) && !catalog.has(entry.id)){
|
|
20
|
-
catalog.set(entry.id,{id:entry.id,name:typeof entry.name==="string"?entry.name.slice(0,90):entry.id});
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
var all=[...catalog.values()].sort((a,b)=>a.id.localeCompare(b.id));
|
|
24
|
-
if(!all.length)return response("error","No provider models available for this API key.");
|
|
25
|
-
var text=String(context.invocation?.args||argumentText||"").trim();
|
|
26
|
-
var config=context.services?.config;
|
|
27
|
-
if(!config||typeof config.setModel!=="function")return response("error","BCCLI model switch is unavailable in this session.");
|
|
28
|
-
var current=config.getModel?.()||process.env.OPENAI_MODEL||"(unknown)";
|
|
29
|
-
var list=(entries,page=1)=>{
|
|
30
|
-
bccliModelLastList=entries.map(x=>x.id);
|
|
31
|
-
var size=12;
|
|
32
|
-
var pages=Math.max(1,Math.ceil(entries.length/size));
|
|
33
|
-
if(!Number.isSafeInteger(page)||page<1||page>pages)return response("error","Invalid page. Use /model page 1 ... "+pages);
|
|
34
|
-
var start=(page-1)*size;
|
|
35
|
-
var rows=entries.slice(start,start+size).map((x,i)=>(start+i+1)+". "+x.id+(x.id===current?" [ACTIVE]":""));
|
|
36
|
-
return response("info","Provider AI models ("+entries.length+" available; page "+page+"/"+pages+")\nActive: "+current+"\n\n"+rows.join("\n")+"\n\nUse /model NUMBER or /model MODEL_ID to switch now; /model page N; /model search KEYWORD. To change providers, use bccli provider use PROVIDER before a new chat.");
|
|
37
|
-
};
|
|
38
|
-
if(!text)return list(all);
|
|
39
|
-
var page=text.match(/^page\s+([0-9]+)$/i);
|
|
40
|
-
if(page)return list(all,Number(page[1]));
|
|
41
|
-
var search=text.match(/^search\s+(.{1,80})$/i);
|
|
42
|
-
if(search)return list(all.filter(x=>x.id.toLowerCase().includes(search[1].toLowerCase())||x.name.toLowerCase().includes(search[1].toLowerCase())));
|
|
43
|
-
var item;
|
|
44
|
-
if(/^[0-9]+$/.test(text)){
|
|
45
|
-
var index=Number(text);
|
|
46
|
-
item=Number.isSafeInteger(index)&&index>=1?catalog.get(bccliModelLastList[index-1]||all[index-1]?.id):undefined;
|
|
47
|
-
}else if(/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/.test(text)){
|
|
48
|
-
item=catalog.get(text);
|
|
49
|
-
}
|
|
50
|
-
if(!item)return response("error","That model is not available in your active provider catalog. Run /model to view allowed models.");
|
|
51
|
-
if(item.id===current)return response("info","Already using "+item.id+" via the active provider.");
|
|
52
|
-
// Raw model IDs are supported by Config.setModel; this preserves OpenAI
|
|
53
|
-
// auth/loopback proxy instead of switching providers. It refreshes the
|
|
54
|
-
// live content generator, so the next message uses the new model.
|
|
55
|
-
await config.setModel(item.id,{reason:"bccli-gateway-model-picker"});
|
|
56
|
-
if(config.getModel?.()!==item.id)return response("error","Model switch did not take effect; previous model preserved.");
|
|
57
|
-
return response("info","Switched to "+item.id+" in the current session. Routing and billing remain with the active provider.");
|
|
58
|
-
}catch{
|
|
59
|
-
return response("error","Provider model selection failed; no upstream provider fallback was used.");
|
|
60
|
-
}
|
|
61
|
-
},"action")};`
|
package/provider-cli.mjs
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Provider management is an explicitly invoked CLI operation, not a model prompt.
|
|
2
|
-
import { PROVIDERS, isConfigured, providerDefinition, removeProvider, saveProviderKey, selectedProvider, useProvider } from './providers.mjs'
|
|
3
|
-
|
|
4
|
-
export async function hiddenApiKey({ input = process.stdin, output = process.stdout } = {}) {
|
|
5
|
-
if (!input.isTTY || typeof input.setRawMode !== 'function') throw Error('Provider add requires an interactive terminal to enter a hidden API key.')
|
|
6
|
-
output.write('Paste API key (hidden; Enter to save, Ctrl+C to cancel): ')
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
let value = ''
|
|
9
|
-
const finish = (error) => {
|
|
10
|
-
input.off('data', onData)
|
|
11
|
-
input.off('end', onEnd)
|
|
12
|
-
input.setRawMode(false)
|
|
13
|
-
input.pause()
|
|
14
|
-
output.write('\n')
|
|
15
|
-
error ? reject(error) : resolve(value)
|
|
16
|
-
}
|
|
17
|
-
const onEnd = () => finish(Error('API key input ended before confirmation.'))
|
|
18
|
-
const onData = buffer => {
|
|
19
|
-
for (const ch of buffer.toString('utf8')) {
|
|
20
|
-
if (ch === '\u0003') return finish(Error('Cancelled.'))
|
|
21
|
-
if (ch === '\r' || ch === '\n') return finish(value ? null : Error('Empty API key; nothing saved.'))
|
|
22
|
-
if (ch === '\u007f' || ch === '\b') { value = value.slice(0, -1); continue }
|
|
23
|
-
if (ch.charCodeAt(0) < 32) continue
|
|
24
|
-
value += ch
|
|
25
|
-
if (value.length > 8192) return finish(Error('API key is too long; nothing saved.'))
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
input.on('data', onData)
|
|
29
|
-
input.on('end', onEnd)
|
|
30
|
-
input.setRawMode(true)
|
|
31
|
-
input.resume()
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function runProviderCommand(words, {
|
|
36
|
-
env = process.env,
|
|
37
|
-
getKey = hiddenApiKey,
|
|
38
|
-
out = text => console.log(text),
|
|
39
|
-
save = saveProviderKey,
|
|
40
|
-
select = useProvider,
|
|
41
|
-
remove = removeProvider,
|
|
42
|
-
configured = isConfigured,
|
|
43
|
-
active = selectedProvider,
|
|
44
|
-
} = {}) {
|
|
45
|
-
const [action = 'list', id, ...extra] = words
|
|
46
|
-
if (!['list', 'add', 'use', 'remove'].includes(action) || (action === 'list' ? words.length > 1 : !id || extra.length)) {
|
|
47
|
-
throw Error('Use: provider list | add PROVIDER | use PROVIDER | remove PROVIDER.')
|
|
48
|
-
}
|
|
49
|
-
if (action === 'list') {
|
|
50
|
-
const current = active({env})
|
|
51
|
-
for (const [name, info] of Object.entries(PROVIDERS)) {
|
|
52
|
-
out(name + ' - ' + info.name + ' [' + (configured(name,{env}) ? 'configured' : 'no key') + ']' + (name === current ? ' [ACTIVE]' : ''))
|
|
53
|
-
}
|
|
54
|
-
out('Add: bccli provider add openrouter; select: bccli provider use openrouter')
|
|
55
|
-
out('Provider keys are stored encrypted for your Windows user. Other systems: use the provider environment variable.')
|
|
56
|
-
return 0
|
|
57
|
-
}
|
|
58
|
-
providerDefinition(id)
|
|
59
|
-
if (action === 'add') {
|
|
60
|
-
if (id === 'botconnector') throw Error('For BotConnector, use the existing "bccli-legacy login bc-cloud".')
|
|
61
|
-
out('Provider: ' + PROVIDERS[id].name + ' (direct API; provider billing and terms apply).')
|
|
62
|
-
const key = await getKey()
|
|
63
|
-
save(id, key)
|
|
64
|
-
select(id,{env})
|
|
65
|
-
out('API key securely saved and provider selected: ' + id + '. Key was not printed. No AI request sent.')
|
|
66
|
-
out('Run: bccli models, then bccli chat --model MODEL_ID')
|
|
67
|
-
return 0
|
|
68
|
-
}
|
|
69
|
-
if (action === 'use') {
|
|
70
|
-
select(id,{env})
|
|
71
|
-
out('Active provider: ' + id + '. New sessions only; existing chats are unchanged.')
|
|
72
|
-
return 0
|
|
73
|
-
}
|
|
74
|
-
remove(id,{env})
|
|
75
|
-
out('Saved provider key removed for ' + id + '. Environment variables, if set, are not changed.')
|
|
76
|
-
return 0
|
|
77
|
-
}
|
package/providers.mjs
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// BCCLI Preview provider keys: explicit OpenAI-compatible presets only.
|
|
2
|
-
// Windows stores provider API keys as per-user DPAPI encrypted blobs. The
|
|
3
|
-
// upstream Qwen process receives only an ephemeral loopback proxy token.
|
|
4
|
-
import { randomBytes } from 'node:crypto'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
7
|
-
import { homedir } from 'node:os'
|
|
8
|
-
import { isAbsolute, join } from 'node:path'
|
|
9
|
-
import { botConnectorCredentialStatus, botConnectorKey } from './credentials.mjs'
|
|
10
|
-
import { GATEWAY } from './gateway.mjs'
|
|
11
|
-
|
|
12
|
-
export const PROVIDERS = Object.freeze({
|
|
13
|
-
botconnector: Object.freeze({ name: 'BotConnector Cloud', endpoint: GATEWAY }),
|
|
14
|
-
openai: Object.freeze({ name: 'OpenAI API', endpoint: 'https://api.openai.com/v1', env: 'OPENAI_API_KEY' }),
|
|
15
|
-
openrouter: Object.freeze({ name: 'OpenRouter', endpoint: 'https://openrouter.ai/api/v1', env: 'OPENROUTER_API_KEY' }),
|
|
16
|
-
deepseek: Object.freeze({ name: 'DeepSeek', endpoint: 'https://api.deepseek.com/v1', env: 'DEEPSEEK_API_KEY' }),
|
|
17
|
-
groq: Object.freeze({ name: 'Groq', endpoint: 'https://api.groq.com/openai/v1', env: 'GROQ_API_KEY' }),
|
|
18
|
-
xai: Object.freeze({ name: 'xAI', endpoint: 'https://api.x.ai/v1', env: 'XAI_API_KEY' }),
|
|
19
|
-
})
|
|
20
|
-
export const ALLOWED_PROVIDER_ENDPOINTS = Object.freeze(Object.values(PROVIDERS).map(p => p.endpoint))
|
|
21
|
-
const ID = /^[a-z][a-z0-9-]{0,31}$/
|
|
22
|
-
const MAX_BLOB_BYTES = 64 * 1024
|
|
23
|
-
const ENC_SCRIPT = '$ErrorActionPreference="Stop"; $plain=[Console]::In.ReadToEnd().Trim(); if(-not $plain){exit 1}; $sec=ConvertTo-SecureString -String $plain -AsPlainText -Force; [Console]::Out.Write((ConvertFrom-SecureString -SecureString $sec))'
|
|
24
|
-
const DEC_SCRIPT = '$ErrorActionPreference="Stop"; $encoded=[Console]::In.ReadToEnd().Trim(); $sec=ConvertTo-SecureString -String $encoded; $ptr=[Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec); try{[Console]::Out.Write([Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr))}finally{[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)}'
|
|
25
|
-
|
|
26
|
-
export function providerRoot({ home = homedir(), env = process.env } = {}) {
|
|
27
|
-
const path = env.BCCLI_PREVIEW_HOME || join(home, '.bccli-preview')
|
|
28
|
-
if (!isAbsolute(path)) throw Error('BCCLI_PREVIEW_HOME must be absolute.')
|
|
29
|
-
return path
|
|
30
|
-
}
|
|
31
|
-
export function providerDefinition(id) {
|
|
32
|
-
if (!Object.hasOwn(PROVIDERS, id)) throw Error('Unknown provider. Choose from: ' + Object.keys(PROVIDERS).join(', '))
|
|
33
|
-
return PROVIDERS[id]
|
|
34
|
-
}
|
|
35
|
-
function ownerDirectory(root, { create = false } = {}) {
|
|
36
|
-
if (create) mkdirSync(root, { recursive: true, mode: 0o700 })
|
|
37
|
-
if (!existsSync(root)) return false
|
|
38
|
-
const info = lstatSync(root)
|
|
39
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw Error('Preview credential directory is not a safe regular directory.')
|
|
40
|
-
return true
|
|
41
|
-
}
|
|
42
|
-
function keyPath(root, id) {
|
|
43
|
-
if (!ID.test(id) || id === 'botconnector') throw Error('Provider key storage requires a supported non-BotConnector provider.')
|
|
44
|
-
return join(root, 'provider-' + id + '.dpapi')
|
|
45
|
-
}
|
|
46
|
-
function dpapi(script, input, { platform = process.platform, runner = spawnSync } = {}) {
|
|
47
|
-
if (platform !== 'win32') throw Error('Persisting provider API keys currently requires Windows DPAPI. Use the provider environment variable on other systems.')
|
|
48
|
-
const result = runner('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
49
|
-
input, encoding: 'utf8', timeout: 15000, maxBuffer: MAX_BLOB_BYTES, windowsHide: true,
|
|
50
|
-
env: { ...process.env, PSModulePath: process.env.PSModulePath },
|
|
51
|
-
})
|
|
52
|
-
if (result.error || result.status !== 0 || !result.stdout?.trim()) throw Error('Windows protected credential operation failed; no key was printed.')
|
|
53
|
-
return result.stdout.trim()
|
|
54
|
-
}
|
|
55
|
-
export function saveProviderKey(id, key, { root = providerRoot(), platform = process.platform, runner = spawnSync } = {}) {
|
|
56
|
-
providerDefinition(id)
|
|
57
|
-
if (id === 'botconnector') throw Error('Use bccli-legacy login bc-cloud or BOTCONNECTOR_API_KEY for BotConnector credentials.')
|
|
58
|
-
if (typeof key !== 'string' || !key.trim() || key.length > 8192 || /[\r\n]/.test(key)) throw Error('API key cannot be empty, multiline, or oversized.')
|
|
59
|
-
if (platform !== 'win32') throw Error('On non-Windows systems, use the provider API key environment variable.')
|
|
60
|
-
ownerDirectory(root, { create: true })
|
|
61
|
-
const ciphertext = dpapi(ENC_SCRIPT, key.trim(), { platform, runner })
|
|
62
|
-
if (ciphertext.length > MAX_BLOB_BYTES || !/^[0-9a-f]+$/i.test(ciphertext)) throw Error('Windows credential encryption returned an invalid format.')
|
|
63
|
-
const path = keyPath(root, id)
|
|
64
|
-
if (existsSync(path) && (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())) throw Error('Unsafe provider credentials path.')
|
|
65
|
-
const temporary = path + '.' + randomBytes(8).toString('hex') + '.tmp'
|
|
66
|
-
try {
|
|
67
|
-
writeFileSync(temporary, ciphertext + '\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 })
|
|
68
|
-
renameSync(temporary, path)
|
|
69
|
-
} finally { rmSync(temporary, { force: true }) }
|
|
70
|
-
}
|
|
71
|
-
export function storedProviderKey(id, { root = providerRoot(), platform = process.platform, runner = spawnSync } = {}) {
|
|
72
|
-
providerDefinition(id)
|
|
73
|
-
if (id === 'botconnector') throw Error('BotConnector credentials are managed by public BCCLI.')
|
|
74
|
-
if (!ownerDirectory(root)) throw Error('Provider not configured. Run provider add.')
|
|
75
|
-
const path = keyPath(root, id)
|
|
76
|
-
let info
|
|
77
|
-
try { info = lstatSync(path) }
|
|
78
|
-
catch { throw Error('Provider not configured. Run provider add.') }
|
|
79
|
-
if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_BLOB_BYTES) throw Error('Unsafe provider credentials file.')
|
|
80
|
-
const cipher = readFileSync(path, 'utf8')
|
|
81
|
-
if (!/^[0-9a-f]+\s*$/i.test(cipher)) throw Error('Provider credentials are invalid.')
|
|
82
|
-
return dpapi(DEC_SCRIPT, cipher, { platform, runner })
|
|
83
|
-
}
|
|
84
|
-
export function isConfigured(id, { env = process.env, root = providerRoot({env}) } = {}) {
|
|
85
|
-
providerDefinition(id)
|
|
86
|
-
if (id === 'botconnector') return botConnectorCredentialStatus({ env }).configured
|
|
87
|
-
if (env[PROVIDERS[id].env]?.trim()) return true
|
|
88
|
-
try { return ownerDirectory(root) && lstatSync(keyPath(root, id)).isFile() && !lstatSync(keyPath(root, id)).isSymbolicLink() }
|
|
89
|
-
catch { return false }
|
|
90
|
-
}
|
|
91
|
-
export function selectedProvider({ env = process.env, root = providerRoot({env}) } = {}) {
|
|
92
|
-
if (!ownerDirectory(root)) return 'botconnector'
|
|
93
|
-
const path = join(root, 'active-provider')
|
|
94
|
-
if (!existsSync(path)) return 'botconnector'
|
|
95
|
-
const info = lstatSync(path)
|
|
96
|
-
if (!info.isFile() || info.isSymbolicLink() || info.size > 128) throw Error('Unsafe provider selection file.')
|
|
97
|
-
const id = readFileSync(path, 'utf8').trim()
|
|
98
|
-
providerDefinition(id)
|
|
99
|
-
return id
|
|
100
|
-
}
|
|
101
|
-
// Model IDs are public selections, never credentials. Keep one per provider
|
|
102
|
-
// so a returning user goes directly to chat without repeating the model picker.
|
|
103
|
-
const MODEL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/
|
|
104
|
-
export function selectedModel(id, { env = process.env, root = providerRoot({ env }) } = {}) {
|
|
105
|
-
providerDefinition(id)
|
|
106
|
-
if (!ownerDirectory(root)) return undefined
|
|
107
|
-
const path = join(root, 'active-model-' + id)
|
|
108
|
-
if (!existsSync(path)) return undefined
|
|
109
|
-
const info = lstatSync(path)
|
|
110
|
-
if (!info.isFile() || info.isSymbolicLink() || info.size > 300) throw Error('Unsafe model selection file.')
|
|
111
|
-
const value = readFileSync(path, 'utf8').trim()
|
|
112
|
-
if (!MODEL_ID.test(value)) throw Error('Invalid saved model selection.')
|
|
113
|
-
return value
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function saveSelectedModel(id, model, { env = process.env, root = providerRoot({ env }) } = {}) {
|
|
117
|
-
providerDefinition(id)
|
|
118
|
-
if (!MODEL_ID.test(model)) throw Error('Invalid model ID.')
|
|
119
|
-
ownerDirectory(root, { create: true })
|
|
120
|
-
const path = join(root, 'active-model-' + id)
|
|
121
|
-
if (existsSync(path) && (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())) {
|
|
122
|
-
throw Error('Unsafe model selection path.')
|
|
123
|
-
}
|
|
124
|
-
const tmp = path + '.' + randomBytes(8).toString('hex') + '.tmp'
|
|
125
|
-
try { writeFileSync(tmp, model + '\n', { flag: 'wx', encoding: 'utf8', mode: 0o600 }); renameSync(tmp, path) }
|
|
126
|
-
finally { rmSync(tmp, { force: true }) }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function useProvider(id, { env = process.env, root = providerRoot({env}) } = {}) {
|
|
130
|
-
providerDefinition(id)
|
|
131
|
-
if (!isConfigured(id, { env, root })) throw Error('Provider is not configured. Use: provider add ' + id + ' (or its API key environment variable).')
|
|
132
|
-
ownerDirectory(root, { create: true })
|
|
133
|
-
const path = join(root, 'active-provider')
|
|
134
|
-
if (existsSync(path) && (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())) throw Error('Unsafe provider selection path.')
|
|
135
|
-
const tmp = path + '.' + randomBytes(8).toString('hex') + '.tmp'
|
|
136
|
-
try { writeFileSync(tmp, id + '\n', { flag: 'wx', encoding: 'utf8', mode: 0o600 }); renameSync(tmp, path) }
|
|
137
|
-
finally { rmSync(tmp, { force: true }) }
|
|
138
|
-
}
|
|
139
|
-
export function removeProvider(id, { env = process.env, root = providerRoot({env}) } = {}) {
|
|
140
|
-
providerDefinition(id)
|
|
141
|
-
if (id === 'botconnector') throw Error('BotConnector credentials are managed by bccli-legacy or BOTCONNECTOR_API_KEY.')
|
|
142
|
-
if (!ownerDirectory(root)) return
|
|
143
|
-
const path = keyPath(root, id)
|
|
144
|
-
if (existsSync(path)) {
|
|
145
|
-
if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink()) throw Error('Unsafe provider credential path.')
|
|
146
|
-
rmSync(path)
|
|
147
|
-
}
|
|
148
|
-
if (selectedProvider({env,root}) === id) {
|
|
149
|
-
const active = join(root, 'active-provider')
|
|
150
|
-
rmSync(active, {force:true})
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
export function resolveProvider(id, { env = process.env, root = providerRoot({env}) } = {}) {
|
|
154
|
-
const info = providerDefinition(id)
|
|
155
|
-
if (id === 'botconnector') return { id, ...info, key: botConnectorKey({env}) }
|
|
156
|
-
// An explicit key saved through "provider add" must win over stale environment
|
|
157
|
-
// variables. Never silently fall back if the encrypted file is corrupted.
|
|
158
|
-
let key
|
|
159
|
-
try { key = storedProviderKey(id, {root}) }
|
|
160
|
-
catch (error) {
|
|
161
|
-
if (error?.message !== 'Provider not configured. Run provider add.') throw error
|
|
162
|
-
key = env[info.env]?.trim()
|
|
163
|
-
}
|
|
164
|
-
if (!key) throw Error('Provider not configured. Run provider add ' + id + ' or set ' + info.env + '.')
|
|
165
|
-
return { id, ...info, key }
|
|
166
|
-
}
|