@botconnector/bccli 0.4.0-beta.36 → 0.4.0-beta.37

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 CHANGED
@@ -15,6 +15,7 @@ cd project-kamu && bccli
15
15
  |---|---|
16
16
  | `bccli` | mode interaktif |
17
17
  | `bccli -p "tugas"` | satu tugas tanpa interaksi (skrip/CI) |
18
+ | `bccli -p "tugas" --output-format json` | hasil akhir sebagai satu objek JSON; `stream-json` = event JSONL per tahap (tool_use/tool_result/message/result) untuk CI |
18
19
  | `bccli -c` / `bccli -r` | lanjutkan sesi terakhir / pilih sesi |
19
20
  | `bccli -m provider/model` | pilih model |
20
21
  | `bccli --reasoning <auto|off|low|medium|high|max>` | pilih level reasoning untuk invocation ini |
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ helpText,
4
+ parseCliArgs
5
+ } from "./chunk-ASTNW3X2.js";
6
+ import "./chunk-R3ND76LW.js";
7
+ import "./chunk-YURAIW7A.js";
8
+ import "./chunk-H5N4ST6B.js";
9
+ import "./chunk-4RMP4CNT.js";
10
+ export {
11
+ helpText,
12
+ parseCliArgs
13
+ };
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/oauth.ts
4
+ import { spawn } from "child_process";
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { createServer } from "http";
7
+ import { dirname, join } from "path";
8
+ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
9
+ var oauthStorePath = (home) => join(home, "mcp-oauth.json");
10
+ function readStore(home) {
11
+ const path = oauthStorePath(home);
12
+ if (!existsSync(path)) return {};
13
+ try {
14
+ return JSON.parse(readFileSync(path, "utf8"));
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+ function writeStore(home, store) {
20
+ const path = oauthStorePath(home);
21
+ mkdirSync(dirname(path), { recursive: true });
22
+ writeFileSync(path, `${JSON.stringify(store, null, 2)}
23
+ `, { mode: 384 });
24
+ chmodSync(path, 384);
25
+ }
26
+ function hasStoredTokens(home, serverUrl) {
27
+ return Boolean(readStore(home)[serverUrl]?.tokens);
28
+ }
29
+ function clearTokens(home, serverUrl) {
30
+ const store = readStore(home);
31
+ const entry = store[serverUrl];
32
+ if (!entry?.tokens && !entry?.client && !entry?.codeVerifier && !entry?.discovery) return false;
33
+ delete store[serverUrl];
34
+ writeStore(home, store);
35
+ return true;
36
+ }
37
+ function defaultOpen(url) {
38
+ const target = url.href;
39
+ const child = process.platform === "win32" ? spawn("cmd", ["/c", "start", "", target], { detached: true, stdio: "ignore" }) : spawn(process.platform === "darwin" ? "open" : "xdg-open", [target], { detached: true, stdio: "ignore" });
40
+ child.unref();
41
+ child.on("error", () => {
42
+ });
43
+ }
44
+ var FileOAuthProvider = class {
45
+ opts;
46
+ constructor(opts) {
47
+ this.opts = opts;
48
+ }
49
+ /**
50
+ * Always defined: MCP SDK treats a missing redirectUrl as a non-interactive
51
+ * (client_credentials) flow and would try a token exchange without a code.
52
+ */
53
+ get redirectUrl() {
54
+ return this.opts.redirectUrl ?? "http://127.0.0.1:1/callback";
55
+ }
56
+ get clientMetadata() {
57
+ return {
58
+ client_name: "BCCLI",
59
+ redirect_uris: [String(this.redirectUrl)],
60
+ grant_types: ["authorization_code", "refresh_token"],
61
+ response_types: ["code"],
62
+ token_endpoint_auth_method: "none"
63
+ };
64
+ }
65
+ entry() {
66
+ return readStore(this.opts.home)[this.opts.serverUrl] ?? {};
67
+ }
68
+ patch(patch) {
69
+ const store = readStore(this.opts.home);
70
+ const next = { ...store[this.opts.serverUrl], ...patch };
71
+ for (const key of Object.keys(next)) {
72
+ if (next[key] === void 0) delete next[key];
73
+ }
74
+ store[this.opts.serverUrl] = next;
75
+ if (Object.keys(next).length === 0) delete store[this.opts.serverUrl];
76
+ writeStore(this.opts.home, store);
77
+ }
78
+ clientInformation() {
79
+ return this.entry().client;
80
+ }
81
+ saveClientInformation(clientInformation) {
82
+ this.patch({ client: clientInformation });
83
+ }
84
+ tokens() {
85
+ return this.entry().tokens;
86
+ }
87
+ saveTokens(tokens) {
88
+ this.patch({ tokens });
89
+ }
90
+ saveCodeVerifier(codeVerifier) {
91
+ this.patch({ codeVerifier });
92
+ }
93
+ codeVerifier() {
94
+ return this.entry().codeVerifier ?? "";
95
+ }
96
+ redirectToAuthorization(authorizationUrl) {
97
+ return (this.opts.open ?? defaultOpen)(authorizationUrl);
98
+ }
99
+ saveDiscoveryState(state) {
100
+ this.patch({ discovery: state });
101
+ }
102
+ discoveryState() {
103
+ return this.entry().discovery;
104
+ }
105
+ invalidateCredentials(scope) {
106
+ if (scope === "all") {
107
+ this.patch({ client: void 0, tokens: void 0, codeVerifier: void 0, discovery: void 0 });
108
+ return;
109
+ }
110
+ if (scope === "client") this.patch({ client: void 0 });
111
+ if (scope === "tokens") this.patch({ tokens: void 0 });
112
+ if (scope === "verifier") this.patch({ codeVerifier: void 0 });
113
+ if (scope === "discovery") this.patch({ discovery: void 0 });
114
+ }
115
+ };
116
+ function parseCallback(input) {
117
+ const url = typeof input === "string" ? new URL(input) : input;
118
+ const error = url.searchParams.get("error");
119
+ if (error) {
120
+ const description = url.searchParams.get("error_description");
121
+ throw new Error(description ? `OAuth ${error}: ${description}` : `OAuth ${error}`);
122
+ }
123
+ const code = url.searchParams.get("code");
124
+ if (!code) throw new Error("OAuth callback is missing the code parameter.");
125
+ const state = url.searchParams.get("state") ?? void 0;
126
+ return { code, state };
127
+ }
128
+ var CALLBACK_TIMEOUT_MS = 18e4;
129
+ async function authorizeMcpServer(spec, deps) {
130
+ let settle;
131
+ const codeReceived = new Promise((resolve, reject) => {
132
+ settle = { resolve, reject };
133
+ });
134
+ codeReceived.catch(() => {
135
+ });
136
+ const server = createServer((req, res) => {
137
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
138
+ if (url.pathname !== "/callback") {
139
+ res.writeHead(404);
140
+ res.end();
141
+ return;
142
+ }
143
+ try {
144
+ const { code } = parseCallback(url);
145
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
146
+ res.end("<html><body><p>BCCLI is connected. You can close this tab.</p></body></html>");
147
+ settle.resolve(code);
148
+ } catch (error) {
149
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
150
+ res.end(`<html><body><p>${error.message}</p></body></html>`);
151
+ settle.reject(error);
152
+ }
153
+ });
154
+ const port = await new Promise((resolve, reject) => {
155
+ server.once("error", reject);
156
+ server.listen(0, "127.0.0.1", () => {
157
+ const address = server.address();
158
+ resolve(typeof address === "object" && address ? address.port : 0);
159
+ });
160
+ });
161
+ try {
162
+ const provider = new FileOAuthProvider({
163
+ home: deps.home,
164
+ serverUrl: spec.config.url,
165
+ redirectUrl: `http://127.0.0.1:${port}/callback`,
166
+ open: deps.open
167
+ });
168
+ const serverUrl = new URL(spec.config.url);
169
+ const result = await auth(provider, { serverUrl, fetchFn: deps.fetchFn });
170
+ if (result === "AUTHORIZED") return;
171
+ const timer = setTimeout(() => settle.reject(new Error("Timed out waiting for the OAuth callback.")), CALLBACK_TIMEOUT_MS);
172
+ let code;
173
+ try {
174
+ code = await codeReceived;
175
+ } finally {
176
+ clearTimeout(timer);
177
+ }
178
+ const second = await auth(provider, { serverUrl, authorizationCode: code, fetchFn: deps.fetchFn });
179
+ if (second !== "AUTHORIZED") throw new Error("OAuth authorization did not complete.");
180
+ } finally {
181
+ server.close();
182
+ }
183
+ }
184
+
185
+ export {
186
+ hasStoredTokens,
187
+ clearTokens,
188
+ FileOAuthProvider,
189
+ authorizeMcpServer
190
+ };
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ contentText
4
+ } from "./chunk-7TABTC32.js";
2
5
 
3
6
  // src/session.ts
4
7
  import { randomBytes } from "crypto";
@@ -36,7 +39,7 @@ var Session = class _Session {
36
39
  return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
37
40
  const session = new _Session(join(dir, f));
38
41
  const first = session.load().find((m) => m.role === "user");
39
- return { session, mtime: statSync(session.file).mtime, preview: first ? String(first.content).slice(0, 80) : "" };
42
+ return { session, mtime: statSync(session.file).mtime, preview: first ? contentText(first.content).slice(0, 80) : "" };
40
43
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime() || b.session.file.localeCompare(a.session.file));
41
44
  }
42
45
  static latest(home, cwd) {
@@ -1,18 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/presets.ts
4
- var PRESETS = [
5
- { id: "bc-cloud", name: "BotConnector Cloud", baseURL: "https://api.botconnector.id/v1", apiKeyEnv: "BOTCONNECTOR_API_KEY" },
6
- { id: "openrouter", name: "OpenRouter", baseURL: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY" },
7
- { id: "openai", name: "OpenAI", baseURL: "https://api.openai.com/v1", apiKeyEnv: "OPENAI_API_KEY" },
8
- { id: "gemini", name: "Google Gemini", baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", apiKeyEnv: "GEMINI_API_KEY" },
9
- { id: "deepseek", name: "DeepSeek", baseURL: "https://api.deepseek.com/v1", apiKeyEnv: "DEEPSEEK_API_KEY" },
10
- { id: "groq", name: "Groq", baseURL: "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_API_KEY" },
11
- { id: "ollama-cloud", name: "Ollama Cloud", baseURL: "https://ollama.com/v1", apiKeyEnv: "OLLAMA_API_KEY" },
12
- { id: "ollama", name: "Ollama lokal", baseURL: "http://127.0.0.1:11434/v1", local: true },
13
- { id: "lmstudio", name: "LM Studio lokal", baseURL: "http://127.0.0.1:1234/v1", local: true }
14
- ];
15
-
16
3
  // src/i18n/id.ts
17
4
  var ID = {
18
5
  "MCP servers (enter: install / remove \xB7 esc close) \xB7 \u2713 active \u25CB not installed \u2717 error": "Server MCP (enter: pasang / hapus \xB7 esc tutup) \xB7 \u2713 aktif \u25CB belum \u2717 error",
@@ -68,6 +55,15 @@ var ID = {
68
55
  "File: {file}": "File: {file}",
69
56
  "git is not installed or not on PATH, so /diff cannot be used.": "git tidak terpasang atau tidak ada di PATH, jadi /diff tidak bisa dipakai.",
70
57
  "This folder is not a git repository, so there is no diff.": "Folder ini bukan repository git, jadi tidak ada diff.",
58
+ "git is not installed or not on PATH, so /worktree cannot be used.": "git tidak terpasang atau tidak ada di PATH, jadi /worktree tidak bisa dipakai.",
59
+ "git is not installed or not on PATH, so -w cannot be used.": "git tidak terpasang atau tidak ada di PATH, jadi -w tidak bisa dipakai.",
60
+ "This folder is not a git repository, so -w cannot create a worktree.": "Folder ini bukan repository git, jadi -w tidak bisa membuat worktree.",
61
+ 'Invalid worktree name "{name}": use letters, digits, dot, dash, or underscore (no slashes).': 'Nama worktree "{name}" tidak valid: pakai huruf, angka, titik, strip, atau garis bawah (tanpa slash).',
62
+ "Worktree path {path} already exists but is not a registered worktree; remove or rename it first.": "Path worktree {path} sudah ada tapi bukan worktree terdaftar; hapus atau ganti nama dulu.",
63
+ "Git worktrees:": "Git worktree:",
64
+ "Create one with: bccli -w <name>": "Buat dengan: bccli -w <nama>",
65
+ "Remove with: git worktree remove <path>": "Hapus dengan: git worktree remove <path>",
66
+ "list git worktrees; create one with bccli -w <name>": "daftar git worktree; buat dengan bccli -w <nama>",
71
67
  "No uncommitted changes.": "Tidak ada perubahan yang belum di-commit.",
72
68
  "\u2026 {n} more lines": "\u2026 {n} baris lagi",
73
69
  "New files (untracked): {files}": "File baru (belum di-track): {files}",
@@ -109,7 +105,7 @@ Usage:
109
105
  bccli login [provider] save an API key (default: bc-cloud)
110
106
  bccli models list models from the active provider
111
107
  bccli provider list|add <id>|remove <id> manage providers (custom: --url <url> [--name N] [--key-env ENV])
112
- bccli mcp list|add <name>|remove <name> manage MCP servers (catalog, or --url <url>)
108
+ bccli mcp list|add <name>|remove <name>|auth <name>|logout <name> manage MCP servers (catalog, --url <url>, OAuth)
113
109
  bccli integrations show the status of external agent integrations
114
110
  bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible, openclaw, hermes
115
111
  bccli disconnect <agent> remove an integration and restore the previous config
@@ -121,6 +117,8 @@ Options:
121
117
  -r, --resume pick a session to continue
122
118
  --allow-all run all tools without asking for permission
123
119
  --allowed-tools <a,b> tools allowed without asking: bash, edit, fetch
120
+ -w, --worktree <name> run in git worktree ../<repo>.worktrees/<name> (created, or reused if it exists)
121
+ --output-format <fmt> text | json | stream-json for -p (machine-readable output)
124
122
  --permission-mode <mode> default | acceptEdits | plan | allowAll
125
123
  --lang <en|id> interface language (default: en; or BCCLI_LANG, or "language" in config)
126
124
  -v, --version version
@@ -132,7 +130,7 @@ Pemakaian:
132
130
  bccli login [provider] simpan API key (default: bc-cloud)
133
131
  bccli models daftar model dari provider aktif
134
132
  bccli provider list|add <id>|remove <id> kelola provider (custom: --url <url> [--name N] [--key-env ENV])
135
- bccli mcp list|add <nama>|remove <nama> kelola server MCP (katalog, atau --url <url>)
133
+ bccli mcp list|add <nama>|remove <nama>|auth <nama>|logout <nama> kelola server MCP (katalog, --url <url>, OAuth)
136
134
  bccli integrations lihat status integrasi agent eksternal
137
135
  bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible, openclaw, hermes
138
136
  bccli disconnect <agent> lepas integrasi dan pulihkan config sebelumnya
@@ -144,18 +142,32 @@ Opsi:
144
142
  -r, --resume pilih sesi untuk dilanjutkan
145
143
  --allow-all jalankan semua alat tanpa minta izin
146
144
  --allowed-tools <a,b> alat yang boleh tanpa izin: bash, edit, fetch
145
+ -w, --worktree <name> jalankan di git worktree ../<repo>.worktrees/<name> (dibuat, atau dipakai ulang jika ada)
146
+ --output-format <fmt> text | json | stream-json untuk -p (output mesin)
147
147
  --permission-mode <mode> default | acceptEdits | plan | allowAll
148
148
  --lang <en|id> bahasa tampilan (default: en; atau BCCLI_LANG, atau "language" di config)
149
149
  -v, --version versi
150
150
  -h, --help bantuan ini`,
151
151
  "--reasoning must be one of: {levels}": "--reasoning harus salah satu dari: {levels}",
152
+ 'networkPolicy must be "allow" or "offline".': 'networkPolicy harus "allow" atau "offline".',
153
+ "plugins must be an array of strings.": "plugins harus berupa array of string.",
154
+ "Plugin not found: {spec}": "Plugin tidak ditemukan: {spec}",
155
+ "Plugin failed to load: {spec}: {reason}": "Plugin gagal dimuat: {spec}: {reason}",
156
+ "Plugin {spec} registered a tool name that is already in use: {name}": "Plugin {spec} mendaftarkan nama tool yang sudah terpakai: {name}",
152
157
  "--permission-mode must be one of: {modes}": "--permission-mode harus salah satu dari: {modes}",
153
158
  "Plan needs approval; run with --allow-all.": "Rencana butuh persetujuan; jalankan dengan --allow-all.",
154
159
  "Step limit reached.": "Batas langkah tercapai.",
155
160
  "Needs permission for {tool}. Run with --allow-all or --allowed-tools {kind}.": "Butuh izin untuk {tool}. Jalankan dengan --allow-all atau --allowed-tools {kind}.",
156
161
  "--lang must be one of: en, id": "--lang harus salah satu dari: en, id",
162
+ "--output-format must be one of: {formats}": "--output-format harus salah satu dari: {formats}",
157
163
  "Invalid config: {path} ({error})": "Config tidak valid: {path} ({error})",
158
164
  "maxSteps must be a positive number, or 0/null to disable the limit.": "maxSteps harus berupa angka positif, atau 0/null untuk menonaktifkan batas.",
165
+ "Hook {event} failed: {reason}": "Hook {event} gagal: {reason}",
166
+ "list background tasks": "daftar tugas latar belakang",
167
+ "Nothing to rewind.": "Tidak ada yang bisa di-rewind.",
168
+ "Rewound {n} turn(s): the conversation continues from before them.": "{n} giliran di-rewind: percakapan dilanjutkan dari sebelum giliran itu.",
169
+ "step back turns: conversation and file edits (/rewind 2)": "mundur beberapa giliran: percakapan + edit file (/rewind 2)",
170
+ "review a pull request (/pr <n|url|branch>)": "review pull request (/pr <n|url|branch>)",
159
171
  "Model must be in provider/model format, e.g. bc-cloud/glm-5.3-flash (got: {ref})": "Model harus berformat provider/model, contoh bc-cloud/glm-5.3-flash (dapat: {ref})",
160
172
  'Provider "{id}" is not in the config. Available: {list}': 'Provider "{id}" tidak ada di config. Tersedia: {list}',
161
173
  "No API key for {id} yet. Run `bccli login` or set the env var {env}.": "API key untuk {id} belum ada. Jalankan `bccli login` atau set env {env}.",
@@ -172,6 +184,13 @@ Opsi:
172
184
  "Usage: bccli provider {action} <id>": "Pemakaian: bccli provider {action} <id>",
173
185
  "{id} removed.": "{id} dihapus.",
174
186
  "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 logout.": "Aksi tidak dikenal: {action}. Pakai list, add, remove, auth, atau logout.",
188
+ "OAuth required for {name}: run bccli mcp auth {name}": "OAuth diperlukan untuk {name}: jalankan bccli mcp auth {name}",
189
+ "{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
+ "{name} authorized. Active in the next bccli session.": "{name} terautorisasi. Aktif di sesi bccli berikutnya.",
191
+ "OAuth login failed for {name}: {reason}": "Login OAuth gagal untuk {name}: {reason}",
192
+ "{name} logged out.": "{name} telah logout.",
193
+ "{name} has no stored OAuth tokens.": "{name} tidak menyimpan token OAuth.",
175
194
  "{id} is not a preset. For a custom provider add --url <OpenAI-compatible base URL>.": "{id} bukan preset. Untuk provider custom tambahkan --url <base URL OpenAI-compatible>.",
176
195
  "API key for {id} (leave empty if not needed): ": "API key untuk {id} (kosongkan bila tidak perlu): ",
177
196
  "{id} is ready. {n} models available.": "{id} siap. {n} model tersedia.",
@@ -316,7 +335,9 @@ Opsi:
316
335
  "{n} characters": "{n} karakter",
317
336
  "{input} tokens in \xB7 {output} tokens out": "{input} token masuk \xB7 {output} token keluar",
318
337
  "Run: dsh web": "Jalankan: dsh web",
319
- "timeout after {n} seconds": "timeout {n} detik"
338
+ "timeout after {n} seconds": "timeout {n} detik",
339
+ "Could not start MCP servers: {error}": "Tidak bisa menjalankan server MCP: {error}",
340
+ "Blocked: {host} resolves to a private address ({addr}). Fetching an internal host through a public DNS name is not allowed.": "Diblokir: {host} mengarah ke alamat privat ({addr}). Mengambil host internal lewat nama DNS publik tidak diizinkan."
320
341
  };
321
342
 
322
343
  // src/i18n.ts
@@ -348,118 +369,11 @@ function resolveLanguage(argv, env, fromConfig) {
348
369
  return langFromArgv(argv) ?? parseLang(env.BCCLI_LANG) ?? parseLang(fromConfig) ?? "en";
349
370
  }
350
371
 
351
- // src/config.ts
352
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
353
- import { homedir } from "os";
354
- import { join } from "path";
355
- var ConfigError = class extends Error {
356
- };
357
- var DEFAULT_CONFIG = {
358
- model: "bc-cloud/glm-5.3-flash",
359
- permissionMode: "default",
360
- reasoning: "auto",
361
- providers: Object.fromEntries(PRESETS.map((p) => [p.id, { baseURL: p.baseURL, apiKeyEnv: p.apiKeyEnv }])),
362
- allow: []
363
- };
364
- function bccliHome(env = process.env) {
365
- return env.BCCLI_HOME ?? join(homedir(), ".bccli");
366
- }
367
- function readJsonConfig(path) {
368
- if (!existsSync(path)) return {};
369
- try {
370
- return JSON.parse(readFileSync(path, "utf8"));
371
- } catch (error) {
372
- throw new ConfigError(t("Invalid config: {path} ({error})", { path, error: error.message }));
373
- }
374
- }
375
- function resolvedMaxSteps(project, global) {
376
- const hasProjectValue = Object.hasOwn(project, "maxSteps");
377
- const value = hasProjectValue ? project.maxSteps : global.maxSteps;
378
- if (value == null || value === 0) return void 0;
379
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
380
- throw new ConfigError(t("maxSteps must be a positive number, or 0/null to disable the limit."));
381
- }
382
- return Math.max(1, Math.floor(value));
383
- }
384
- function untrustedProviders(project, known) {
385
- const out = {};
386
- for (const [id, provider] of Object.entries(project.providers ?? {})) {
387
- if (known[id] || typeof provider?.baseURL !== "string") continue;
388
- out[id] = { baseURL: provider.baseURL };
389
- }
390
- return out;
391
- }
392
- function loadConfig(cwd, env = process.env) {
393
- const global = readJsonConfig(join(bccliHome(env), "config.json"));
394
- const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
395
- const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
396
- const fromProject = untrustedProviders(project, known);
397
- return {
398
- model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
399
- permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
400
- reasoning: global.reasoning ?? DEFAULT_CONFIG.reasoning,
401
- maxSteps: resolvedMaxSteps(project, global),
402
- providers: { ...known, ...fromProject },
403
- projectProviders: Object.keys(fromProject),
404
- allow: [...global.allow ?? []]
405
- };
406
- }
407
- function readCredentials(env = process.env) {
408
- const path = join(bccliHome(env), "credentials");
409
- if (!existsSync(path)) return {};
410
- try {
411
- return JSON.parse(readFileSync(path, "utf8"));
412
- } catch {
413
- return {};
414
- }
415
- }
416
- function saveCredential(providerId, key, env = process.env) {
417
- const home = bccliHome(env);
418
- mkdirSync(home, { recursive: true });
419
- const path = join(home, "credentials");
420
- writeFileSync(path, JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), { mode: 384 });
421
- chmodSync(path, 384);
422
- }
423
- function resolveModel(config, modelRef, env = process.env) {
424
- const slash = modelRef.indexOf("/");
425
- if (slash <= 0 || slash === modelRef.length - 1) {
426
- throw new ConfigError(t("Model must be in provider/model format, e.g. bc-cloud/glm-5.3-flash (got: {ref})", { ref: modelRef }));
427
- }
428
- const providerId = modelRef.slice(0, slash);
429
- const provider = config.providers[providerId];
430
- if (!provider) {
431
- throw new ConfigError(t('Provider "{id}" is not in the config. Available: {list}', { id: providerId, list: Object.keys(config.providers).join(", ") }));
432
- }
433
- const apiKey = (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[providerId] || void 0;
434
- if (provider.apiKeyEnv && !apiKey) {
435
- throw new ConfigError(t("No API key for {id} yet. Run `bccli login` or set the env var {env}.", { id: providerId, env: provider.apiKeyEnv }));
436
- }
437
- return { providerId, model: modelRef.slice(slash + 1), baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey };
438
- }
439
- function removeCredential(providerId, env = process.env) {
440
- const creds = readCredentials(env);
441
- if (!(providerId in creds)) return false;
442
- delete creds[providerId];
443
- const path = join(bccliHome(env), "credentials");
444
- writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 384 });
445
- chmodSync(path, 384);
446
- return true;
447
- }
448
-
449
372
  export {
450
- PRESETS,
451
373
  parseLang,
452
374
  setLanguage,
453
375
  getLanguage,
454
376
  locale,
455
377
  t,
456
- resolveLanguage,
457
- ConfigError,
458
- bccliHome,
459
- readJsonConfig,
460
- loadConfig,
461
- readCredentials,
462
- saveCredential,
463
- resolveModel,
464
- removeCredential
378
+ resolveLanguage
465
379
  };