@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/proxy.mjs
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
2
|
-
import { createServer } from 'node:http'
|
|
3
|
-
import { GATEWAY } from './gateway.mjs'
|
|
4
|
-
import { ALLOWED_PROVIDER_ENDPOINTS } from './providers.mjs'
|
|
5
|
-
|
|
6
|
-
// This proxy lives only for one Qwen Code session. Real provider keys remain in
|
|
7
|
-
// the launcher process; MCP and bash subprocesses can see only a short-lived
|
|
8
|
-
// local bearer token. The proxy cannot be redirected to arbitrary hosts.
|
|
9
|
-
const ALLOWED_PATHS = new Set(['/v1/models', '/v1/chat/completions', '/v1/responses'])
|
|
10
|
-
const MAX_REQUEST_BYTES = 16 * 1024 * 1024
|
|
11
|
-
|
|
12
|
-
// Qwen 0.26.3 produces get_goal without parameters and read_file with
|
|
13
|
-
// optional JSON-schema type unions such as ["integer", "null"]. Strict
|
|
14
|
-
// OpenAI-compatible providers reject both formats while parsing tools.
|
|
15
|
-
// Normalize only the incompatible pieces, preserving all coding tools.
|
|
16
|
-
function normalizeOptionalNullableSchema(schema) {
|
|
17
|
-
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema
|
|
18
|
-
let out = schema
|
|
19
|
-
if (schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)) {
|
|
20
|
-
const required = new Set(Array.isArray(schema.required) ? schema.required : [])
|
|
21
|
-
let properties = schema.properties
|
|
22
|
-
for (const [name, field] of Object.entries(schema.properties)) {
|
|
23
|
-
if (!field || typeof field !== 'object' || Array.isArray(field)) continue
|
|
24
|
-
let candidate = field
|
|
25
|
-
// The optional absence of a field already represents null; dropping
|
|
26
|
-
// "null" from this one optional JSON-Schema union is safe. Do not rewrite
|
|
27
|
-
// required nullable fields or multi-type unions (fail closed upstream).
|
|
28
|
-
if (!required.has(name) && Array.isArray(field.type) && field.type.length === 2
|
|
29
|
-
&& field.type.includes('null')) {
|
|
30
|
-
const primitive = field.type.find((type) => type !== 'null')
|
|
31
|
-
if (['string', 'number', 'integer', 'boolean', 'object', 'array'].includes(primitive)) {
|
|
32
|
-
candidate = { ...field, type: primitive }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
candidate = normalizeOptionalNullableSchema(candidate)
|
|
36
|
-
if (candidate !== field) {
|
|
37
|
-
if (properties === schema.properties) properties = { ...properties }
|
|
38
|
-
properties[name] = candidate
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (properties !== schema.properties) out = { ...out, properties }
|
|
42
|
-
}
|
|
43
|
-
if (schema.items && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
|
|
44
|
-
const items = normalizeOptionalNullableSchema(schema.items)
|
|
45
|
-
if (items !== schema.items) out = { ...out, items }
|
|
46
|
-
}
|
|
47
|
-
return out
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function normalizeZeroArgFunctionTools(requestBody) {
|
|
51
|
-
if (!requestBody || typeof requestBody !== 'object' || !Array.isArray(requestBody.tools)) return requestBody
|
|
52
|
-
let changed = false
|
|
53
|
-
const tools = requestBody.tools.map((tool) => {
|
|
54
|
-
const fn = tool?.type === 'function' ? tool.function : null
|
|
55
|
-
if (!fn || typeof fn.name !== 'string') return tool
|
|
56
|
-
const parameters = fn.parameters == null
|
|
57
|
-
? { type: 'object', properties: {} }
|
|
58
|
-
: normalizeOptionalNullableSchema(fn.parameters)
|
|
59
|
-
if (parameters === fn.parameters) return tool
|
|
60
|
-
changed = true
|
|
61
|
-
return { ...tool, function: { ...fn, parameters } }
|
|
62
|
-
})
|
|
63
|
-
return changed ? { ...requestBody, tools } : requestBody
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// The model can be switched with /model without restarting the coding agent.
|
|
67
|
-
// Annotate each request with the ACTUAL model chosen by Qwen Config, rather
|
|
68
|
-
// than retaining a stale --append-system-prompt model from CLI startup.
|
|
69
|
-
export function attachRuntimeModelContext(payload) {
|
|
70
|
-
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.messages)) return payload
|
|
71
|
-
const model = payload.model
|
|
72
|
-
if (typeof model !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._:/+-]{0,255}$/.test(model)) return payload
|
|
73
|
-
const index = payload.messages.findIndex(item => item?.role === 'system' && typeof item.content === 'string')
|
|
74
|
-
if (index === -1) return payload
|
|
75
|
-
const runtime = '\n\nRuntime metadata (not model instructions): active_model_id=' + model + '; client_app=BCCLI; transport=local-proxy.'
|
|
76
|
-
const messages = [...payload.messages]
|
|
77
|
-
messages[index] = { ...messages[index], content: messages[index].content + runtime }
|
|
78
|
-
return { ...payload, messages }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Qwen 0.26.3 sends max_tokens=64000 for many model IDs. Several Gateway
|
|
82
|
-
// upstream routes reject that value (e.g. GLM Flash). Bound the requested
|
|
83
|
-
// output conservatively for coding-agent interoperability; never increase it.
|
|
84
|
-
// This does not change the context window or input-token budget.
|
|
85
|
-
const DEFAULT_OUTPUT_TOKEN_CEILING = 4096
|
|
86
|
-
export function boundChatOutputTokens(payload) {
|
|
87
|
-
if (!payload || typeof payload !== 'object') return payload
|
|
88
|
-
const legacy = payload.max_tokens
|
|
89
|
-
const modern = payload.max_completion_tokens
|
|
90
|
-
const valid = (value) => Number.isInteger(value) && value > 0
|
|
91
|
-
|
|
92
|
-
// Real Gateway's exact Kimi route rejects legacy max_tokens. Keep parity
|
|
93
|
-
// with PAYG validation for positive decimal numeric strings, without
|
|
94
|
-
// changing the output-ceiling policy for any other model.
|
|
95
|
-
if (payload.model === 'kimi-k2.7-code') {
|
|
96
|
-
const parsed = (value) => {
|
|
97
|
-
if (typeof value !== 'number' && !(typeof value === 'string' && /^[0-9]+$/.test(value))) return null
|
|
98
|
-
const n = Number(value)
|
|
99
|
-
return Number.isSafeInteger(n) && n > 0 ? n : null
|
|
100
|
-
}
|
|
101
|
-
const legacyValue = parsed(legacy)
|
|
102
|
-
const modernValue = parsed(modern)
|
|
103
|
-
if (legacyValue != null) {
|
|
104
|
-
const requested = modernValue != null ? Math.min(legacyValue, modernValue) : legacyValue
|
|
105
|
-
const { max_tokens: _ignored, ...rest } = payload
|
|
106
|
-
return { ...rest, max_completion_tokens: Math.min(requested, DEFAULT_OUTPUT_TOKEN_CEILING) }
|
|
107
|
-
}
|
|
108
|
-
if (modernValue != null && (typeof modern !== 'number' || modernValue > DEFAULT_OUTPUT_TOKEN_CEILING)) {
|
|
109
|
-
return { ...payload, max_completion_tokens: Math.min(modernValue, DEFAULT_OUTPUT_TOKEN_CEILING) }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (valid(legacy) && legacy > DEFAULT_OUTPUT_TOKEN_CEILING) {
|
|
113
|
-
return { ...payload, max_tokens: DEFAULT_OUTPUT_TOKEN_CEILING }
|
|
114
|
-
}
|
|
115
|
-
if (valid(modern) && modern > DEFAULT_OUTPUT_TOKEN_CEILING) {
|
|
116
|
-
return { ...payload, max_completion_tokens: DEFAULT_OUTPUT_TOKEN_CEILING }
|
|
117
|
-
}
|
|
118
|
-
return payload
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function waitForDrainOrClose(res) {
|
|
122
|
-
if (res.destroyed) return Promise.resolve()
|
|
123
|
-
return new Promise((resolve) => {
|
|
124
|
-
const done = () => {
|
|
125
|
-
res.off('drain', done)
|
|
126
|
-
res.off('close', done)
|
|
127
|
-
resolve()
|
|
128
|
-
}
|
|
129
|
-
res.once('drain', done)
|
|
130
|
-
res.once('close', done)
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function matchesToken(auth, token) {
|
|
135
|
-
if (typeof auth !== 'string') return false
|
|
136
|
-
const actual = Buffer.from(auth)
|
|
137
|
-
const expected = Buffer.from('Bearer ' + token)
|
|
138
|
-
return actual.length === expected.length && timingSafeEqual(actual, expected)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function startGatewayProxy({ key, endpoint = GATEWAY, token = randomBytes(32).toString('hex'), fetchImpl = fetch } = {}) {
|
|
142
|
-
if (!ALLOWED_PROVIDER_ENDPOINTS.includes(endpoint)) throw Error('Unapproved provider endpoint; local proxy refused to start.')
|
|
143
|
-
if (typeof key !== 'string' || !key.trim()) throw Error('Gateway key is required for local proxy.')
|
|
144
|
-
if (typeof token !== 'string' || token.length < 6 || token === key) throw Error('Invalid or non-isolated proxy token.')
|
|
145
|
-
|
|
146
|
-
const server = createServer(async (req, res) => {
|
|
147
|
-
const fail = (status, body) => {
|
|
148
|
-
if (res.headersSent || res.destroyed) return
|
|
149
|
-
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
150
|
-
res.end(JSON.stringify({ error: body }))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!matchesToken(req.headers.authorization, token)) {
|
|
154
|
-
fail(401, 'Local session token is required.')
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
if (!ALLOWED_PATHS.has(req.url ?? '')) {
|
|
158
|
-
fail(404, 'Unsupported local gateway route.')
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
const method = req.url === '/v1/models' ? 'GET' : 'POST'
|
|
162
|
-
if (req.method !== method) {
|
|
163
|
-
fail(405, 'Unsupported request method.')
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
if (req.headers['content-length'] && Number(req.headers['content-length']) > MAX_REQUEST_BYTES) {
|
|
167
|
-
fail(413, 'Request too large.')
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const abort = new AbortController()
|
|
172
|
-
const onClose = () => { if (!res.writableEnded) abort.abort() }
|
|
173
|
-
res.once('close', onClose)
|
|
174
|
-
try {
|
|
175
|
-
let body
|
|
176
|
-
if (method === 'POST') {
|
|
177
|
-
const buffers = []
|
|
178
|
-
let bytes = 0
|
|
179
|
-
for await (const chunk of req) {
|
|
180
|
-
bytes += chunk.length
|
|
181
|
-
if (bytes > MAX_REQUEST_BYTES) {
|
|
182
|
-
fail(413, 'Request too large.')
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
buffers.push(chunk)
|
|
186
|
-
}
|
|
187
|
-
body = Buffer.concat(buffers)
|
|
188
|
-
if (req.url === '/v1/chat/completions') {
|
|
189
|
-
let parsed
|
|
190
|
-
try { parsed = JSON.parse(body.toString('utf8')) }
|
|
191
|
-
catch { fail(400, 'Invalid JSON request.'); return }
|
|
192
|
-
const normalized = boundChatOutputTokens(attachRuntimeModelContext(normalizeZeroArgFunctionTools(parsed)))
|
|
193
|
-
if (normalized !== parsed) body = Buffer.from(JSON.stringify(normalized))
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const upstream = await fetchImpl(endpoint + req.url.slice(3), {
|
|
197
|
-
method,
|
|
198
|
-
headers: {
|
|
199
|
-
authorization: 'Bearer ' + key,
|
|
200
|
-
accept: 'application/json, text/event-stream',
|
|
201
|
-
...(method === 'POST' ? { 'content-type': 'application/json' } : {}),
|
|
202
|
-
},
|
|
203
|
-
body,
|
|
204
|
-
redirect: 'error',
|
|
205
|
-
signal: abort.signal,
|
|
206
|
-
})
|
|
207
|
-
res.writeHead(upstream.status, {
|
|
208
|
-
'content-type': upstream.headers.get('content-type') ?? 'application/json',
|
|
209
|
-
'cache-control': 'no-store',
|
|
210
|
-
})
|
|
211
|
-
if (upstream.body) {
|
|
212
|
-
for await (const chunk of upstream.body) {
|
|
213
|
-
if (res.destroyed) break
|
|
214
|
-
if (!res.write(chunk)) await waitForDrainOrClose(res)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
res.end()
|
|
218
|
-
} catch {
|
|
219
|
-
if (res.headersSent) res.destroy()
|
|
220
|
-
else fail(502, 'Provider connection failed.')
|
|
221
|
-
} finally {
|
|
222
|
-
res.off('close', onClose)
|
|
223
|
-
}
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
await new Promise((resolve, reject) => {
|
|
227
|
-
const onError = (error) => { server.off('listening', onListening); reject(error) }
|
|
228
|
-
const onListening = () => { server.off('error', onError); resolve() }
|
|
229
|
-
server.once('error', onError)
|
|
230
|
-
server.once('listening', onListening)
|
|
231
|
-
server.listen(0, '127.0.0.1')
|
|
232
|
-
})
|
|
233
|
-
const port = server.address()?.port
|
|
234
|
-
return {
|
|
235
|
-
token,
|
|
236
|
-
baseUrl: 'http://127.0.0.1:' + port + '/v1',
|
|
237
|
-
async close() {
|
|
238
|
-
server.closeAllConnections()
|
|
239
|
-
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
|
240
|
-
},
|
|
241
|
-
}
|
|
242
|
-
}
|
package/startup-menu.mjs
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// In-terminal BCCLI first-run / provider-switch menu.
|
|
2
|
-
// This runs in the parent process BEFORE the coding agent is launched; real
|
|
3
|
-
// API keys never enter Qwen's process or any coding/MCP subprocess.
|
|
4
|
-
import { createInterface } from 'node:readline/promises'
|
|
5
|
-
import { PROVIDERS, isConfigured, resolveProvider, selectedProvider, selectedModel, saveSelectedModel, saveProviderKey, useProvider } from './providers.mjs'
|
|
6
|
-
import { hiddenApiKey } from './provider-cli.mjs'
|
|
7
|
-
import { DEFAULT_MODEL, discoverModels, selectModel } from './core.mjs'
|
|
8
|
-
|
|
9
|
-
export const PROVIDER_SWITCH_EXIT = 51
|
|
10
|
-
|
|
11
|
-
// Returning users go straight to chat with the selected provider and model.
|
|
12
|
-
// Eligibility is still verified against the live provider's available models.
|
|
13
|
-
export async function autoSelectStartup({
|
|
14
|
-
providerId = selectedProvider(),
|
|
15
|
-
configured = isConfigured,
|
|
16
|
-
remembered = selectedModel,
|
|
17
|
-
resolve = resolveProvider,
|
|
18
|
-
list = discoverModels,
|
|
19
|
-
defaultModel = DEFAULT_MODEL,
|
|
20
|
-
env = process.env,
|
|
21
|
-
} = {}) {
|
|
22
|
-
if (!configured(providerId)) return undefined
|
|
23
|
-
const requested = providerId === 'botconnector'
|
|
24
|
-
? (env.BOTCONNECTOR_MODEL_ID || remembered(providerId) || defaultModel)
|
|
25
|
-
: remembered(providerId)
|
|
26
|
-
if (!requested) return undefined
|
|
27
|
-
const profile = resolve(providerId)
|
|
28
|
-
const models = await list({ key: profile.key, endpoint: profile.endpoint })
|
|
29
|
-
if (!models.some(item => item.id === requested)) return undefined
|
|
30
|
-
return { providerId, model: selectModel(models, requested), key: profile.key, endpoint: profile.endpoint }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function terminalQuestion(label, { input = process.stdin, output = process.stdout } = {}) {
|
|
34
|
-
const rl = createInterface({ input, output, terminal: !!input.isTTY })
|
|
35
|
-
try { return (await rl.question(label)).trim() }
|
|
36
|
-
finally { rl.close() }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function selectedName(answer, choices, defaultValue) {
|
|
40
|
-
if (!answer) return defaultValue
|
|
41
|
-
const clean = answer.replace(/!$/, '').trim().toLowerCase()
|
|
42
|
-
if (/^[0-9]+$/.test(clean)) return choices[Number(clean) - 1]
|
|
43
|
-
return choices.includes(clean) ? clean : undefined
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function startupMenu({
|
|
47
|
-
ask = terminalQuestion,
|
|
48
|
-
askSecret = hiddenApiKey,
|
|
49
|
-
out = value => console.log(value),
|
|
50
|
-
currentProvider = selectedProvider,
|
|
51
|
-
configured = isConfigured,
|
|
52
|
-
saveKey = saveProviderKey,
|
|
53
|
-
use = useProvider,
|
|
54
|
-
remembered = selectedModel,
|
|
55
|
-
saveModel = saveSelectedModel,
|
|
56
|
-
resolve = resolveProvider,
|
|
57
|
-
list = discoverModels,
|
|
58
|
-
defaultModel = DEFAULT_MODEL,
|
|
59
|
-
} = {}) {
|
|
60
|
-
const ids = Object.keys(PROVIDERS)
|
|
61
|
-
const active = currentProvider()
|
|
62
|
-
out('')
|
|
63
|
-
out('BCCLI | Provider dan model')
|
|
64
|
-
out('Pilih provider. Masukkan tanda ! setelah angka untuk mengganti API key yang sudah tersimpan.')
|
|
65
|
-
ids.forEach((id, idx) => {
|
|
66
|
-
out(' ' + (idx + 1) + '. ' + PROVIDERS[id].name +
|
|
67
|
-
(configured(id) ? ' [key tersedia]' : ' [perlu API key]') +
|
|
68
|
-
(id === active ? ' [aktif]' : ''))
|
|
69
|
-
})
|
|
70
|
-
let providerId
|
|
71
|
-
let replace
|
|
72
|
-
for (;;) {
|
|
73
|
-
const value = await ask('Provider (angka/nama; Enter = aktif; Ctrl+C untuk batal): ')
|
|
74
|
-
providerId = selectedName(value, ids, active)
|
|
75
|
-
if (!providerId) { out('Pilihan provider tidak dikenal.'); continue }
|
|
76
|
-
replace = value.endsWith('!')
|
|
77
|
-
if (providerId === 'botconnector' && replace) {
|
|
78
|
-
out('Gunakan bccli-legacy login bc-cloud untuk mengubah key BotConnector lama; key tidak akan diubah dari menu ini.')
|
|
79
|
-
continue
|
|
80
|
-
}
|
|
81
|
-
break
|
|
82
|
-
}
|
|
83
|
-
if ((replace || !configured(providerId)) && providerId !== 'botconnector') {
|
|
84
|
-
out('Masukkan API key ' + PROVIDERS[providerId].name + ' (input tersembunyi).')
|
|
85
|
-
const key = await askSecret()
|
|
86
|
-
// A cancel or missing key cannot silently select an unusable provider.
|
|
87
|
-
if (!key) throw Error('Tidak ada API key; provider tidak diubah.')
|
|
88
|
-
saveKey(providerId, key)
|
|
89
|
-
out('API key disimpan terlindungi untuk akun Windows. Tidak ada request AI.')
|
|
90
|
-
}
|
|
91
|
-
if (!configured(providerId)) throw Error('Provider ' + providerId + ' belum memiliki API key. Tidak ada permintaan AI.')
|
|
92
|
-
const profile = resolve(providerId)
|
|
93
|
-
const models = await list({ key: profile.key, endpoint: profile.endpoint })
|
|
94
|
-
if (!models.length) throw Error('Tidak ada model untuk provider ini.')
|
|
95
|
-
const all = [...models].sort((a, b) => a.id.localeCompare(b.id))
|
|
96
|
-
const previous = remembered(providerId)
|
|
97
|
-
const preferred = all.some(m => m.id === previous) ? previous :
|
|
98
|
-
providerId === 'botconnector' && all.some(m => m.id === defaultModel) ? defaultModel : undefined
|
|
99
|
-
let filtered = all
|
|
100
|
-
for (;;) {
|
|
101
|
-
out('')
|
|
102
|
-
out('Model ' + PROVIDERS[providerId].name + ' (' + filtered.length + ' dari ' + all.length + ' model):')
|
|
103
|
-
filtered.slice(0, 14).forEach((m, i) => out(' ' + (i + 1) + '. ' + m.id + (m.id === preferred ? ' [default]' : '')))
|
|
104
|
-
if (filtered.length > 14) out(' ... ketik ?kata untuk mencari model.')
|
|
105
|
-
const value = await ask('Model (nomor/ID/?cari' + (preferred ? '/Enter = ' + preferred : '') + '): ')
|
|
106
|
-
if (value.startsWith('?')) {
|
|
107
|
-
const term = value.slice(1).trim().toLowerCase()
|
|
108
|
-
filtered = term ? all.filter(m => m.id.toLowerCase().includes(term) || m.name?.toLowerCase().includes(term)) : all
|
|
109
|
-
if (!filtered.length) { out('Tidak ada model yang cocok.'); filtered = all }
|
|
110
|
-
continue
|
|
111
|
-
}
|
|
112
|
-
const candidate = !value ? preferred : /^[0-9]+$/.test(value) ? filtered[Number(value) - 1]?.id : value
|
|
113
|
-
if (!candidate || !all.some(m => m.id === candidate)) { out('Pilih nomor atau ID model yang tampil.'); continue }
|
|
114
|
-
selectModel(all, candidate)
|
|
115
|
-
// Save active provider only after valid credentials and model are confirmed.
|
|
116
|
-
use(providerId)
|
|
117
|
-
saveModel(providerId, candidate)
|
|
118
|
-
return { providerId, model: candidate, key: profile.key, endpoint: profile.endpoint }
|
|
119
|
-
}
|
|
120
|
-
}
|