@botconnector/bccli 0.4.0-beta.38 → 0.4.0-beta.39
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 +8 -0
- package/dist/acp-4XE2KOIJ.js +114 -0
- package/dist/{args-MNVI2OJI.js → args-BD2PHEKG.js} +4 -4
- package/dist/{chunk-TASYQW7B.js → chunk-AVB6M4JE.js} +2 -2
- package/dist/{chunk-OB7FN66F.js → chunk-EONFDWBE.js} +2 -2
- package/dist/{chunk-H66PC2EB.js → chunk-GGNXHJL6.js} +2 -2
- package/dist/chunk-HE33XAPZ.js +341 -0
- package/dist/{chunk-HPZSXX4W.js → chunk-ISCT3PN3.js} +2 -2
- package/dist/{chunk-C6PKRYUX.js → chunk-JROUQ5YS.js} +6 -45
- package/dist/chunk-LN32JIZR.js +56 -0
- package/dist/{chunk-4RMP4CNT.js → chunk-M23QBX3V.js} +52 -12
- package/dist/chunk-M7LLJQ7M.js +310 -0
- package/dist/{chunk-YURAIW7A.js → chunk-NI5JZUZ3.js} +1 -1
- package/dist/chunk-PDZPPSLU.js +8 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-QETGQXPL.js +2125 -0
- package/dist/chunk-RV74TCEP.js +231 -0
- package/dist/{chunk-U66JJ6RN.js → chunk-SESBH44L.js} +1 -1
- package/dist/{chunk-XFTBML3H.js → chunk-VM7HYAJS.js} +1 -1
- package/dist/{chunk-ASTNW3X2.js → chunk-ZG7HCXYA.js} +89 -9
- package/dist/{chunk-HXSREA7S.js → chunk-ZMU7MXCH.js} +184 -295
- package/dist/cli.js +65 -2097
- package/dist/{config-JRO5EOKZ.js → config-C4KLLDC6.js} +3 -3
- package/dist/{integrationCli-ZTZTQD2I.js → integrationCli-HR7CDUSI.js} +2 -2
- package/dist/{login-THRU4U4L.js → login-IWBRUNT4.js} +1 -1
- package/dist/{mcpCli-5ZSAMBJS.js → mcpCli-5QTXH7XJ.js} +9 -5
- package/dist/modelsCli-DKTT3UZD.js +36 -0
- package/dist/providerCli-DDH4ZZUY.js +15 -0
- package/dist/sdk.js +5114 -0
- package/dist/serve-7BP5BVX3.js +514 -0
- package/dist/serve-I2VQUTHR.js +178 -0
- package/dist/session-H2YL5RSB.js +12 -0
- package/dist/setup-FGCFKC7Z.js +23 -0
- package/dist/{ui-W4C6SRHA.js → ui-MLOXPAHT.js} +292 -54
- package/dist/update-73WI27VM.js +109 -0
- package/package.json +5 -2
- package/dist/chunk-H5N4ST6B.js +0 -149
- package/dist/providerCli-TGDE3633.js +0 -14
- package/dist/session-TVOWVP6Y.js +0 -12
|
@@ -31,7 +31,7 @@ var ID = {
|
|
|
31
31
|
"\u2026 {n} earlier steps": "\u2026 {n} langkah sebelumnya",
|
|
32
32
|
"\u2717 failed": "\u2717 gagal",
|
|
33
33
|
"\u2713 done": "\u2713 selesai",
|
|
34
|
-
"\u2026 {n} more lines (
|
|
34
|
+
"\u2026 {n} more lines ({bind})": "\u2026 {n} baris lagi ({bind})",
|
|
35
35
|
"No sessions in this folder yet; starting a new one.": "Belum ada sesi di folder ini; memulai sesi baru.",
|
|
36
36
|
"Session number (enter = new): ": "Nomor sesi (enter = baru): ",
|
|
37
37
|
'This repo wants to run the MCP server "{name}" ({what}). Allow? [y/N] ': 'Repo ini ingin menjalankan server MCP "{name}" ({what}). Izinkan? [y/N] ',
|
|
@@ -82,6 +82,7 @@ var ID = {
|
|
|
82
82
|
"version, model, permission mode, MCP, context": "versi, model, mode izin, MCP, konteks",
|
|
83
83
|
"view/revoke permissions": "lihat/cabut izin",
|
|
84
84
|
"undo the file edits of the last turn": "batalkan edit file giliran terakhir",
|
|
85
|
+
"re-apply the file edits that /undo just reverted": "terapkan ulang edit file yang baru dibatalkan /undo",
|
|
85
86
|
"git diff of the project": "git diff project",
|
|
86
87
|
"copy the last answer": "salin jawaban terakhir",
|
|
87
88
|
"save the conversation as markdown": "simpan percakapan ke markdown",
|
|
@@ -103,9 +104,12 @@ Usage:
|
|
|
103
104
|
bccli [task] interactive mode (optionally start with a task)
|
|
104
105
|
bccli -p "task" run one task without interaction (scripts/CI)
|
|
105
106
|
bccli login [provider] save an API key (default: bc-cloud)
|
|
106
|
-
bccli models
|
|
107
|
+
bccli models [provider] list models (default: the active provider)
|
|
108
|
+
bccli update install the newest bccli from npm
|
|
109
|
+
bccli acp run as an ACP agent for editors (Zed, ...)
|
|
110
|
+
bccli serve [--port N] serve sessions over HTTP+WS for IDEs/bots
|
|
107
111
|
bccli provider list|add <id>|remove <id> manage providers (custom: --url <url> [--name N] [--key-env ENV])
|
|
108
|
-
bccli mcp list|add <name>|remove <name>|auth <name>|logout <name> manage MCP servers
|
|
112
|
+
bccli mcp list|add <name>|remove <name>|auth <name>|logout <name>|serve [--base-url <url>] manage MCP servers, or serve this account's tools over stdio
|
|
109
113
|
bccli integrations show the status of external agent integrations
|
|
110
114
|
bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible, openclaw, hermes
|
|
111
115
|
bccli disconnect <agent> remove an integration and restore the previous config
|
|
@@ -128,9 +132,12 @@ Pemakaian:
|
|
|
128
132
|
bccli [tugas] mode interaktif (opsional langsung dengan tugas)
|
|
129
133
|
bccli -p "tugas" jalankan satu tugas tanpa interaksi (skrip/CI)
|
|
130
134
|
bccli login [provider] simpan API key (default: bc-cloud)
|
|
131
|
-
bccli models
|
|
135
|
+
bccli models [provider] daftar model (default: provider aktif)
|
|
136
|
+
bccli update pasang bccli terbaru dari npm
|
|
137
|
+
bccli acp jalankan sebagai agen ACP untuk editor (Zed, ...)
|
|
138
|
+
bccli serve [--port N] sajikan sesi lewat HTTP+WS untuk IDE/bot
|
|
132
139
|
bccli provider list|add <id>|remove <id> kelola provider (custom: --url <url> [--name N] [--key-env ENV])
|
|
133
|
-
bccli mcp list|add <nama>|remove <nama>|auth <nama>|logout <nama> kelola server MCP
|
|
140
|
+
bccli mcp list|add <nama>|remove <nama>|auth <nama>|logout <nama>|serve [--base-url <url>] kelola server MCP, atau layani tools akun ini lewat stdio
|
|
134
141
|
bccli integrations lihat status integrasi agent eksternal
|
|
135
142
|
bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible, openclaw, hermes
|
|
136
143
|
bccli disconnect <agent> lepas integrasi dan pulihkan config sebelumnya
|
|
@@ -149,8 +156,38 @@ Opsi:
|
|
|
149
156
|
-v, --version versi
|
|
150
157
|
-h, --help bantuan ini`,
|
|
151
158
|
"--reasoning must be one of: {levels}": "--reasoning harus salah satu dari: {levels}",
|
|
159
|
+
"--port must be a number between 0 and 65535": "--port harus angka antara 0 dan 65535",
|
|
160
|
+
"serve only accepts loopback --host (127.0.0.1, localhost, ::1)": "serve hanya menerima --host loopback (127.0.0.1, localhost, ::1)",
|
|
152
161
|
'networkPolicy must be "allow" or "offline".': 'networkPolicy harus "allow" atau "offline".',
|
|
153
162
|
"plugins must be an array of strings.": "plugins harus berupa array of string.",
|
|
163
|
+
"verifyCommands must be an array of non-empty strings.": "verifyCommands harus berupa array of string tidak kosong.",
|
|
164
|
+
'usageCap must be an object, e.g. { "tokens": 2000000 }.': 'usageCap harus berupa objek, mis. { "tokens": 2000000 }.',
|
|
165
|
+
"usageCap.tokens must be a number greater than 0.": "usageCap.tokens harus angka lebih dari 0.",
|
|
166
|
+
"usageCap.usd must be a number greater than 0.": "usageCap.usd harus angka lebih dari 0.",
|
|
167
|
+
"usageCap.usd requires usageCap.prices (USD per 1M tokens).": "usageCap.usd memerlukan usageCap.prices (USD per 1 juta token).",
|
|
168
|
+
'usageCap.prices["{key}"] must have numeric input and output (USD per 1M tokens).': 'usageCap.prices["{key}"] harus punya input dan output numerik (USD per 1 juta token).',
|
|
169
|
+
"usageCap needs at least one of: tokens, usd.": "usageCap memerlukan setidaknya salah satu dari: tokens, usd.",
|
|
170
|
+
'updateCheck must be "on" or "off".': 'updateCheck harus "on" atau "off".',
|
|
171
|
+
"repoMap must be true or false.": "repoMap harus true atau false.",
|
|
172
|
+
"theme must be one of: {themes}.": "theme harus salah satu dari: {themes}.",
|
|
173
|
+
'editor must be "emacs" or "vim".': 'editor harus "emacs" atau "vim".',
|
|
174
|
+
'sandbox must be "off" or "on".': 'sandbox harus "off" atau "on".',
|
|
175
|
+
'lsp must be an object with a "servers" array.': 'lsp harus berupa objek dengan array "servers".',
|
|
176
|
+
'Each lsp server needs { "extensions": [".ts"], "command": "...", "args": ["..."] }.': 'Setiap server lsp butuh { "extensions": [".ts"], "command": "...", "args": ["..."] }.',
|
|
177
|
+
"No LSP server is configured for {ext} files.": "Tidak ada server LSP yang dikonfigurasi untuk file {ext}.",
|
|
178
|
+
NORMAL: "-- NORMAL --",
|
|
179
|
+
"A new bccli is available: {latest} (you have {current}). Run: bccli update": "bccli versi baru tersedia: {latest} (kamu punya {current}). Jalankan: bccli update",
|
|
180
|
+
"Clipboard image saved to {path} \u2014 the path is inserted in the input; the model sees it when you send.": "Gambar clipboard disimpan ke {path} \u2014 path disisipkan ke input; model melihatnya saat kamu kirim.",
|
|
181
|
+
"No image on the clipboard.": "Tidak ada gambar di clipboard.",
|
|
182
|
+
"Budget exceeded ({kind}): {used} of {limit}. Further model calls are blocked; raise usageCap in ~/.bccli/config.json.": "Anggaran terlampaui ({kind}): {used} dari {limit}. Panggilan model selanjutnya diblokir; naikkan usageCap di ~/.bccli/config.json.",
|
|
183
|
+
"Verifying edits: {cmds}": "Memverifikasi edit: {cmds}",
|
|
184
|
+
"Verification passed.": "Verifikasi lulus.",
|
|
185
|
+
"Verification still failing after {n} fix round(s). Run the commands manually to see why.": "Verifikasi masih gagal setelah {n} ronde perbaikan. Jalankan perintahnya secara manual untuk melihat penyebabnya.",
|
|
186
|
+
"Automatic verification after your edits failed. Fix the problems so these commands pass, then stop.\n\n{blocks}": "Verifikasi otomatis setelah editmu gagal. Perbaiki masalahnya agar perintah-perintah ini lulus, lalu berhenti.\n\n{blocks}",
|
|
187
|
+
"keybinds must be an object mapping action to binding.": "keybinds harus berupa objek yang memetakan aksi ke binding.",
|
|
188
|
+
'Keybind for "{action}" must be a string like "ctrl+t".': 'Keybind untuk "{action}" harus string seperti "ctrl+t".',
|
|
189
|
+
'Keybind "{spec}" for "{action}" is invalid: use ctrl or alt plus one letter, e.g. "ctrl+t".': 'Keybind "{spec}" untuk "{action}" tidak valid: gunakan ctrl atau alt plus satu huruf, mis. "ctrl+t".',
|
|
190
|
+
'Unknown keybind action "{action}". Known actions: {list}.': 'Aksi keybind "{action}" tidak dikenal. Aksi yang dikenal: {list}.',
|
|
154
191
|
"Plugin not found: {spec}": "Plugin tidak ditemukan: {spec}",
|
|
155
192
|
"Plugin failed to load: {spec}: {reason}": "Plugin gagal dimuat: {spec}: {reason}",
|
|
156
193
|
"Plugin {spec} registered a tool name that is already in use: {name}": "Plugin {spec} mendaftarkan nama tool yang sudah terpakai: {name}",
|
|
@@ -184,7 +221,7 @@ Opsi:
|
|
|
184
221
|
"Usage: bccli provider {action} <id>": "Pemakaian: bccli provider {action} <id>",
|
|
185
222
|
"{id} removed.": "{id} dihapus.",
|
|
186
223
|
"Unknown action: {action}. Use list, add, or remove.": "Aksi tidak dikenal: {action}. Pakai list, add, atau remove.",
|
|
187
|
-
"Unknown action: {action}. Use list, add, remove, auth, or
|
|
224
|
+
"Unknown action: {action}. Use list, add, remove, auth, logout, or serve.": "Aksi tidak dikenal: {action}. Pakai list, add, remove, auth, logout, atau serve.",
|
|
188
225
|
"OAuth required for {name}: run bccli mcp auth {name}": "OAuth diperlukan untuk {name}: jalankan bccli mcp auth {name}",
|
|
189
226
|
"{name} is not an installed HTTP MCP server. Add it first with: bccli mcp add {name} --url <url>": "{name} bukan server MCP HTTP yang terpasang. Tambahkan dulu: bccli mcp add {name} --url <url>",
|
|
190
227
|
"{name} authorized. Active in the next bccli session.": "{name} terautorisasi. Aktif di sesi bccli berikutnya.",
|
|
@@ -254,8 +291,8 @@ Opsi:
|
|
|
254
291
|
"Custom commands:": "Perintah custom:",
|
|
255
292
|
"Skills:": "Skill:",
|
|
256
293
|
"shift+tab switch permission mode esc cancel the turn": "shift+tab ganti mode izin esc batalkan giliran",
|
|
257
|
-
"
|
|
258
|
-
"
|
|
294
|
+
"{bind} full output of the last tool \\ + enter new line": "{bind} output alat terakhir lengkap \\ + enter baris baru",
|
|
295
|
+
"{bind} show/hide the model\u2019s thinking": "{bind} buka/tutup thinking model",
|
|
259
296
|
"@file + tab complete file names \u2191\u2193 input history": "@file + tab lengkapi nama file \u2191\u2193 riwayat input",
|
|
260
297
|
"custom command": "perintah custom",
|
|
261
298
|
"skill \xB7 {description}": "skill \xB7 {description}",
|
|
@@ -290,6 +327,9 @@ Opsi:
|
|
|
290
327
|
"No stored key for {provider}.": "Tidak ada key tersimpan untuk {provider}.",
|
|
291
328
|
"Key for {provider} deleted. Use /login or /provider to add one again.": "Key {provider} dihapus. Pakai /login atau /provider untuk menambah lagi.",
|
|
292
329
|
"No file edits to undo.": "Tidak ada edit file yang bisa dibatalkan.",
|
|
330
|
+
"No file edits to redo.": "Tidak ada edit file yang bisa di-redo.",
|
|
331
|
+
"Re-applied: {files}": "Diterapkan ulang: {files}",
|
|
332
|
+
"Deleted again: {files}": "Dihapus lagi: {files}",
|
|
293
333
|
"Could not restore: {files}": "Gagal dikembalikan: {files}",
|
|
294
334
|
"Restored: {files}": "Dikembalikan: {files}",
|
|
295
335
|
"Deleted (new files): {files}": "Dihapus (file baru): {files}",
|
|
@@ -304,15 +344,15 @@ Opsi:
|
|
|
304
344
|
"Unknown command: /{name}. Type /help.": "Perintah tidak dikenal: /{name}. Ketik /help.",
|
|
305
345
|
"Skill {name} cannot be read: {error}": "Skill {name} tidak bisa dibaca: {error}",
|
|
306
346
|
"{n} queued messages cancelled.": "{n} pesan antrian dibatalkan.",
|
|
307
|
-
"Thinking shown (
|
|
308
|
-
"Thinking hidden (
|
|
347
|
+
"Thinking shown ({bind} to hide).": "Thinking ditampilkan ({bind} untuk menutup).",
|
|
348
|
+
"Thinking hidden ({bind} to show).": "Thinking disembunyikan ({bind} untuk membuka).",
|
|
309
349
|
"MCP {name} removed.": "MCP {name} dihapus.",
|
|
310
350
|
"MCP {name} installed, connecting\u2026": "MCP {name} dipasang, menghubungkan\u2026",
|
|
311
351
|
"MCP {name} active \xB7 {n} tools.": "MCP {name} aktif \xB7 {n} alat.",
|
|
312
352
|
"MCP {name} failed: {error}": "MCP {name} gagal: {error}",
|
|
313
353
|
"unknown": "tidak diketahui",
|
|
314
|
-
"\u273B Thinking \xB7 {n} lines \xB7
|
|
315
|
-
"\u273B Thinking (
|
|
354
|
+
"\u273B Thinking \xB7 {n} lines \xB7 {bind} to show": "\u273B Berpikir \xB7 {n} baris \xB7 {bind} buka",
|
|
355
|
+
"\u273B Thinking ({bind} to hide)": "\u273B Berpikir ({bind} tutup)",
|
|
316
356
|
" \u23F3 queued: {q}": " \u23F3 antri: {q}",
|
|
317
357
|
"Thinking": "Berpikir",
|
|
318
358
|
"Cancelling": "Membatalkan",
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
t
|
|
4
|
+
} from "./chunk-M23QBX3V.js";
|
|
5
|
+
|
|
6
|
+
// src/presets.ts
|
|
7
|
+
var PRESETS = [
|
|
8
|
+
{ id: "bc-cloud", name: "BotConnector Cloud", baseURL: "https://api.botconnector.id/v1", apiKeyEnv: "BOTCONNECTOR_API_KEY" },
|
|
9
|
+
{ id: "openrouter", name: "OpenRouter", baseURL: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY" },
|
|
10
|
+
{ id: "openai", name: "OpenAI", baseURL: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY" },
|
|
11
|
+
{ id: "gemini", name: "Google Gemini", baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", apiKeyEnv: "GEMINI_API_KEY" },
|
|
12
|
+
{ id: "deepseek", name: "DeepSeek", baseURL: "https://api.deepseek.com/v1", apiKeyEnv: "DEEPSEEK_API_KEY" },
|
|
13
|
+
{ id: "groq", name: "Groq", baseURL: "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_API_KEY" },
|
|
14
|
+
{ id: "ollama-cloud", name: "Ollama Cloud", baseURL: "https://ollama.com/v1", apiKeyEnv: "OLLAMA_API_KEY" },
|
|
15
|
+
{ id: "ollama", name: "Ollama lokal", baseURL: "http://127.0.0.1:11434/v1", local: true },
|
|
16
|
+
{ id: "lmstudio", name: "LM Studio lokal", baseURL: "http://127.0.0.1:1234/v1", local: true }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// src/ui/theme.ts
|
|
20
|
+
var THEMES = ["default", "blue", "amber", "magenta", "mono"];
|
|
21
|
+
var REMAP = {
|
|
22
|
+
blue: { green: "blue" },
|
|
23
|
+
amber: { green: "yellow" },
|
|
24
|
+
magenta: { green: "magenta" }
|
|
25
|
+
};
|
|
26
|
+
var theme = "default";
|
|
27
|
+
function setTheme(name) {
|
|
28
|
+
theme = name;
|
|
29
|
+
}
|
|
30
|
+
function color(name) {
|
|
31
|
+
if (process.env.NO_COLOR) return void 0;
|
|
32
|
+
if (theme === "mono") return void 0;
|
|
33
|
+
return REMAP[theme]?.[name] ?? name;
|
|
34
|
+
}
|
|
35
|
+
var ACCENT = "green";
|
|
36
|
+
|
|
37
|
+
// src/config.ts
|
|
38
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
39
|
+
import { homedir } from "os";
|
|
40
|
+
import { join } from "path";
|
|
41
|
+
|
|
42
|
+
// src/keybinds.ts
|
|
43
|
+
var KEYBIND_ACTIONS = ["thinking", "toolOutput", "pasteImage"];
|
|
44
|
+
var DEFAULT_KEYBINDS = {
|
|
45
|
+
thinking: "ctrl+t",
|
|
46
|
+
toolOutput: "ctrl+o",
|
|
47
|
+
pasteImage: "alt+v"
|
|
48
|
+
};
|
|
49
|
+
function defaultKeybinds() {
|
|
50
|
+
return Object.fromEntries(KEYBIND_ACTIONS.map((action) => [action, parseKeybind(DEFAULT_KEYBINDS[action])]));
|
|
51
|
+
}
|
|
52
|
+
function parseKeybind(raw) {
|
|
53
|
+
const spec = raw.trim().toLowerCase();
|
|
54
|
+
const parts = spec.split("+");
|
|
55
|
+
const key = parts.pop() ?? "";
|
|
56
|
+
if (!/^[a-z]$/.test(key)) return void 0;
|
|
57
|
+
if (!parts.length || new Set(parts).size !== parts.length) return void 0;
|
|
58
|
+
if (parts.some((p) => p !== "ctrl" && p !== "alt")) return void 0;
|
|
59
|
+
return { spec, ctrl: parts.includes("ctrl"), meta: parts.includes("alt"), key };
|
|
60
|
+
}
|
|
61
|
+
function matchKeybind(kb, input, key) {
|
|
62
|
+
return kb.ctrl === !!key.ctrl && kb.meta === !!key.meta && input.toLowerCase() === kb.key;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/config.ts
|
|
66
|
+
var ConfigError = class extends Error {
|
|
67
|
+
};
|
|
68
|
+
var DEFAULT_MAX_STEPS = 100;
|
|
69
|
+
var DEFAULT_CONFIG = {
|
|
70
|
+
model: "bc-cloud/glm-5.3-flash",
|
|
71
|
+
permissionMode: "default",
|
|
72
|
+
reasoning: "auto",
|
|
73
|
+
vision: true,
|
|
74
|
+
networkPolicy: "allow",
|
|
75
|
+
keybinds: defaultKeybinds(),
|
|
76
|
+
verifyCommands: [],
|
|
77
|
+
repoMap: false,
|
|
78
|
+
theme: "default",
|
|
79
|
+
editor: "emacs",
|
|
80
|
+
sandbox: "off",
|
|
81
|
+
providers: Object.fromEntries(PRESETS.map((p) => [p.id, { baseURL: p.baseURL, apiKeyEnv: p.apiKeyEnv }])),
|
|
82
|
+
allow: []
|
|
83
|
+
};
|
|
84
|
+
function bccliHome(env = process.env) {
|
|
85
|
+
return env.BCCLI_HOME ?? join(homedir(), ".bccli");
|
|
86
|
+
}
|
|
87
|
+
function readJsonConfig(path) {
|
|
88
|
+
if (!existsSync(path)) return {};
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new ConfigError(t("Invalid config: {path} ({error})", { path, error: error.message }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function resolvedMaxSteps(global) {
|
|
96
|
+
const value = global.maxSteps;
|
|
97
|
+
if (value === void 0) return DEFAULT_MAX_STEPS;
|
|
98
|
+
if (value === null || value === 0) return null;
|
|
99
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
100
|
+
throw new ConfigError(t("maxSteps must be a positive number, or 0/null to disable the limit."));
|
|
101
|
+
}
|
|
102
|
+
return Math.max(1, Math.floor(value));
|
|
103
|
+
}
|
|
104
|
+
function resolvedNetworkPolicy(global) {
|
|
105
|
+
const value = global.networkPolicy ?? "allow";
|
|
106
|
+
if (value !== "allow" && value !== "offline") {
|
|
107
|
+
throw new ConfigError(t('networkPolicy must be "allow" or "offline".'));
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
function resolvedPlugins(global) {
|
|
112
|
+
const value = global.plugins;
|
|
113
|
+
if (value === void 0) return [];
|
|
114
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
115
|
+
throw new ConfigError(t("plugins must be an array of strings."));
|
|
116
|
+
}
|
|
117
|
+
return [...value];
|
|
118
|
+
}
|
|
119
|
+
function resolvedUpdateCheck(global) {
|
|
120
|
+
const value = global.updateCheck;
|
|
121
|
+
if (value === void 0) return void 0;
|
|
122
|
+
if (value !== "on" && value !== "off") throw new ConfigError(t('updateCheck must be "on" or "off".'));
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function resolvedUsageCap(global) {
|
|
126
|
+
const cap = global.usageCap;
|
|
127
|
+
if (cap === void 0) return void 0;
|
|
128
|
+
if (typeof cap !== "object" || cap === null || Array.isArray(cap)) {
|
|
129
|
+
throw new ConfigError(t('usageCap must be an object, e.g. { "tokens": 2000000 }.'));
|
|
130
|
+
}
|
|
131
|
+
const { tokens, usd, prices } = cap;
|
|
132
|
+
const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
133
|
+
const nonNegative = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
134
|
+
if (tokens !== void 0 && !positive(tokens)) throw new ConfigError(t("usageCap.tokens must be a number greater than 0."));
|
|
135
|
+
if (usd !== void 0 && !positive(usd)) throw new ConfigError(t("usageCap.usd must be a number greater than 0."));
|
|
136
|
+
if (usd !== void 0 && (prices === void 0 || !Object.keys(prices).length)) {
|
|
137
|
+
throw new ConfigError(t("usageCap.usd requires usageCap.prices (USD per 1M tokens)."));
|
|
138
|
+
}
|
|
139
|
+
if (prices !== void 0) {
|
|
140
|
+
for (const [key, price] of Object.entries(prices)) {
|
|
141
|
+
if (!price || !nonNegative(price.input) || !nonNegative(price.output)) {
|
|
142
|
+
throw new ConfigError(t('usageCap.prices["{key}"] must have numeric input and output (USD per 1M tokens).', { key }));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (tokens === void 0 && usd === void 0) throw new ConfigError(t("usageCap needs at least one of: tokens, usd."));
|
|
147
|
+
return { ...tokens !== void 0 ? { tokens } : {}, ...usd !== void 0 ? { usd } : {}, ...prices !== void 0 ? { prices } : {} };
|
|
148
|
+
}
|
|
149
|
+
function resolvedLsp(global) {
|
|
150
|
+
if (global.lsp === void 0) return void 0;
|
|
151
|
+
const lsp = global.lsp;
|
|
152
|
+
if (typeof global.lsp !== "object" || global.lsp === null || !Array.isArray(lsp.servers)) {
|
|
153
|
+
throw new ConfigError(t('lsp must be an object with a "servers" array.'));
|
|
154
|
+
}
|
|
155
|
+
for (const s of lsp.servers) {
|
|
156
|
+
const server = s;
|
|
157
|
+
if (typeof server?.command !== "string" || !server.command || !Array.isArray(server.extensions) || server.extensions.length === 0 || server.extensions.some((e) => typeof e !== "string" || !e.startsWith(".")) || server.args !== void 0 && (!Array.isArray(server.args) || server.args.some((a) => typeof a !== "string"))) {
|
|
158
|
+
throw new ConfigError(t('Each lsp server needs { "extensions": [".ts"], "command": "...", "args": ["..."] }.'));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { servers: lsp.servers };
|
|
162
|
+
}
|
|
163
|
+
function resolvedSandbox(global) {
|
|
164
|
+
const value = global.sandbox ?? "off";
|
|
165
|
+
if (value !== "off" && value !== "on") throw new ConfigError(t('sandbox must be "off" or "on".'));
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function resolvedEditor(global) {
|
|
169
|
+
const value = global.editor ?? "emacs";
|
|
170
|
+
if (value !== "emacs" && value !== "vim") throw new ConfigError(t('editor must be "emacs" or "vim".'));
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function resolvedTheme(global) {
|
|
174
|
+
const value = global.theme ?? "default";
|
|
175
|
+
if (typeof value !== "string" || !THEMES.includes(value)) {
|
|
176
|
+
throw new ConfigError(t("theme must be one of: {themes}.", { themes: THEMES.join(", ") }));
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function resolvedRepoMap(global) {
|
|
181
|
+
const value = global.repoMap ?? false;
|
|
182
|
+
if (typeof value !== "boolean") throw new ConfigError(t("repoMap must be true or false."));
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
function resolvedVerifyCommands(global) {
|
|
186
|
+
const value = global.verifyCommands;
|
|
187
|
+
if (value === void 0) return [];
|
|
188
|
+
if (!Array.isArray(value) || value.some((c) => typeof c !== "string" || !c.trim())) {
|
|
189
|
+
throw new ConfigError(t("verifyCommands must be an array of non-empty strings."));
|
|
190
|
+
}
|
|
191
|
+
return value.map((c) => c.trim());
|
|
192
|
+
}
|
|
193
|
+
function resolvedKeybinds(global) {
|
|
194
|
+
const raw = global.keybinds ?? {};
|
|
195
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
196
|
+
throw new ConfigError(t("keybinds must be an object mapping action to binding."));
|
|
197
|
+
}
|
|
198
|
+
const out = {};
|
|
199
|
+
for (const action of KEYBIND_ACTIONS) {
|
|
200
|
+
const spec = raw[action];
|
|
201
|
+
const value = spec === void 0 ? DEFAULT_KEYBINDS[action] : spec;
|
|
202
|
+
if (typeof value !== "string") {
|
|
203
|
+
throw new ConfigError(t('Keybind for "{action}" must be a string like "ctrl+t".', { action }));
|
|
204
|
+
}
|
|
205
|
+
const parsed = parseKeybind(value);
|
|
206
|
+
if (!parsed) {
|
|
207
|
+
throw new ConfigError(t('Keybind "{spec}" for "{action}" is invalid: use ctrl or alt plus one letter, e.g. "ctrl+t".', { spec: value, action }));
|
|
208
|
+
}
|
|
209
|
+
out[action] = parsed;
|
|
210
|
+
}
|
|
211
|
+
for (const key of Object.keys(raw)) {
|
|
212
|
+
if (!KEYBIND_ACTIONS.includes(key)) {
|
|
213
|
+
throw new ConfigError(t('Unknown keybind action "{action}". Known actions: {list}.', { action: key, list: KEYBIND_ACTIONS.join(", ") }));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
function untrustedProviders(project, known) {
|
|
219
|
+
const out = {};
|
|
220
|
+
for (const [id, provider] of Object.entries(project.providers ?? {})) {
|
|
221
|
+
if (known[id] || typeof provider?.baseURL !== "string") continue;
|
|
222
|
+
out[id] = { baseURL: provider.baseURL };
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
function loadConfig(cwd, env = process.env) {
|
|
227
|
+
const global = readJsonConfig(join(bccliHome(env), "config.json"));
|
|
228
|
+
const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
|
|
229
|
+
const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
|
|
230
|
+
const fromProject = untrustedProviders(project, known);
|
|
231
|
+
return {
|
|
232
|
+
model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
|
|
233
|
+
permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
|
|
234
|
+
reasoning: global.reasoning ?? DEFAULT_CONFIG.reasoning,
|
|
235
|
+
maxSteps: resolvedMaxSteps(global),
|
|
236
|
+
hooks: global.hooks,
|
|
237
|
+
vision: global.vision ?? DEFAULT_CONFIG.vision,
|
|
238
|
+
networkPolicy: resolvedNetworkPolicy(global),
|
|
239
|
+
plugins: resolvedPlugins(global),
|
|
240
|
+
keybinds: resolvedKeybinds(global),
|
|
241
|
+
verifyCommands: resolvedVerifyCommands(global),
|
|
242
|
+
repoMap: resolvedRepoMap(global),
|
|
243
|
+
theme: resolvedTheme(global),
|
|
244
|
+
editor: resolvedEditor(global),
|
|
245
|
+
sandbox: resolvedSandbox(global),
|
|
246
|
+
lsp: resolvedLsp(global),
|
|
247
|
+
usageCap: resolvedUsageCap(global),
|
|
248
|
+
updateCheck: resolvedUpdateCheck(global),
|
|
249
|
+
providers: { ...known, ...fromProject },
|
|
250
|
+
projectProviders: Object.keys(fromProject),
|
|
251
|
+
allow: [...global.allow ?? []]
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function readCredentials(env = process.env) {
|
|
255
|
+
const path = join(bccliHome(env), "credentials");
|
|
256
|
+
if (!existsSync(path)) return {};
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
259
|
+
} catch {
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function saveCredential(providerId, key, env = process.env) {
|
|
264
|
+
const home = bccliHome(env);
|
|
265
|
+
mkdirSync(home, { recursive: true });
|
|
266
|
+
const path = join(home, "credentials");
|
|
267
|
+
writeFileSync(path, JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), { mode: 384 });
|
|
268
|
+
chmodSync(path, 384);
|
|
269
|
+
}
|
|
270
|
+
function resolveModel(config, modelRef, env = process.env) {
|
|
271
|
+
const slash = modelRef.indexOf("/");
|
|
272
|
+
if (slash <= 0 || slash === modelRef.length - 1) {
|
|
273
|
+
throw new ConfigError(t("Model must be in provider/model format, e.g. bc-cloud/glm-5.3-flash (got: {ref})", { ref: modelRef }));
|
|
274
|
+
}
|
|
275
|
+
const providerId = modelRef.slice(0, slash);
|
|
276
|
+
const provider = config.providers[providerId];
|
|
277
|
+
if (!provider) {
|
|
278
|
+
throw new ConfigError(t('Provider "{id}" is not in the config. Available: {list}', { id: providerId, list: Object.keys(config.providers).join(", ") }));
|
|
279
|
+
}
|
|
280
|
+
const apiKey = (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[providerId] || void 0;
|
|
281
|
+
if (provider.apiKeyEnv && !apiKey) {
|
|
282
|
+
throw new ConfigError(t("No API key for {id} yet. Run `bccli login` or set the env var {env}.", { id: providerId, env: provider.apiKeyEnv }));
|
|
283
|
+
}
|
|
284
|
+
return { providerId, model: modelRef.slice(slash + 1), baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey };
|
|
285
|
+
}
|
|
286
|
+
function removeCredential(providerId, env = process.env) {
|
|
287
|
+
const creds = readCredentials(env);
|
|
288
|
+
if (!(providerId in creds)) return false;
|
|
289
|
+
delete creds[providerId];
|
|
290
|
+
const path = join(bccliHome(env), "credentials");
|
|
291
|
+
writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
292
|
+
chmodSync(path, 384);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export {
|
|
297
|
+
PRESETS,
|
|
298
|
+
matchKeybind,
|
|
299
|
+
setTheme,
|
|
300
|
+
color,
|
|
301
|
+
ACCENT,
|
|
302
|
+
ConfigError,
|
|
303
|
+
bccliHome,
|
|
304
|
+
readJsonConfig,
|
|
305
|
+
loadConfig,
|
|
306
|
+
readCredentials,
|
|
307
|
+
saveCredential,
|
|
308
|
+
resolveModel,
|
|
309
|
+
removeCredential
|
|
310
|
+
};
|