@botconnector/bccli 0.4.0-beta.35 → 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",
@@ -49,7 +36,7 @@ var ID = {
49
36
  "Session number (enter = new): ": "Nomor sesi (enter = baru): ",
50
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] ',
51
38
  "Conversation summarized to fit the model context.": "Percakapan diringkas agar muat di konteks model.",
52
- 'Step limit of 50 reached. Type "continue" to keep going.': 'Batas 50 langkah tercapai. Ketik "lanjut" untuk meneruskan.',
39
+ 'Step limit of {n} reached. Type "continue" to keep going.': 'Batas {n} langkah tercapai. Ketik "lanjut" untuk meneruskan.',
53
40
  "Cancelled.": "Dibatalkan.",
54
41
  "Permission mode: {mode}": "Mode izin: {mode}",
55
42
  "Folder: {cwd}": "Folder: {cwd}",
@@ -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,9 +105,9 @@ 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
- bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible
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
116
112
 
117
113
  Options:
@@ -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,9 +130,9 @@ 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
- bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible
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
139
137
 
140
138
  Opsi:
@@ -144,17 +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})",
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>)",
158
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})",
159
172
  'Provider "{id}" is not in the config. Available: {list}': 'Provider "{id}" tidak ada di config. Tersedia: {list}',
160
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}.",
@@ -171,6 +184,13 @@ Opsi:
171
184
  "Usage: bccli provider {action} <id>": "Pemakaian: bccli provider {action} <id>",
172
185
  "{id} removed.": "{id} dihapus.",
173
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.",
174
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>.",
175
195
  "API key for {id} (leave empty if not needed): ": "API key untuk {id} (kosongkan bila tidak perlu): ",
176
196
  "{id} is ready. {n} models available.": "{id} siap. {n} model tersedia.",
@@ -195,10 +215,11 @@ Opsi:
195
215
  '-p mode needs a task, e.g. bccli -p "explain this repo"': 'Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"',
196
216
  'Project MCP server "{name}" skipped (not approved yet; run interactive bccli once to approve it).': 'Server MCP project "{name}" dilewati (belum diizinkan; jalankan bccli interaktif sekali untuk menyetujuinya).',
197
217
  'Interactive mode needs a terminal. For scripts/CI use: bccli -p "task"': 'Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"',
198
- "Agent must be one of: opencode, aider, cline, deepseek-harness (dsh), cursor, openai-cli, openai-sdk, openai-compatible, codex, claude-code": "Agent harus salah satu dari: opencode, aider, cline, deepseek-harness (dsh), cursor, openai-cli, openai-sdk, openai-compatible, codex, claude-code",
218
+ "Agent must be one of: opencode, aider, cline, deepseek-harness (dsh), cursor, openai-cli, openai-sdk, openai-compatible, codex, claude-code, openclaw, hermes": "Agent harus salah satu dari: opencode, aider, cline, deepseek-harness (dsh), cursor, openai-cli, openai-sdk, openai-compatible, codex, claude-code, openclaw, hermes",
199
219
  "Cannot determine the home directory.": "Home directory tidak dapat ditentukan.",
200
220
  "No BotConnector API key yet. Run bccli login bc-cloud first.": "API key BotConnector belum tersedia. Jalankan bccli login bc-cloud terlebih dahulu.",
201
221
  "Could not load the BotConnector model catalog (HTTP {status}).": "Katalog model BotConnector gagal dimuat (HTTP {status}).",
222
+ "The BotConnector model catalog is empty; cannot configure external agents.": "Katalog model BotConnector kosong; agent eksternal tidak dapat dikonfigurasi.",
202
223
  "The OpenCode config is not valid JSON: {path}": "Config OpenCode tidak valid JSON: {path}",
203
224
  "OpenCode is connected to BotConnector ({n} models).": "OpenCode terhubung ke BotConnector ({n} model).",
204
225
  "The Aider config already has its own env-file/openai-api settings: {path}. BCCLI will not overwrite them.": "Config Aider sudah memiliki env-file/openai-api setting sendiri: {path}. BCCLI tidak akan menimpanya.",
@@ -219,6 +240,15 @@ Opsi:
219
240
  "Codex already has botconnector.config.toml. BCCLI will not overwrite it.": "Codex sudah memiliki botconnector.config.toml. BCCLI tidak akan menimpanya.",
220
241
  "Codex CLI is connected to the BotConnector Responses API.": "Codex CLI terhubung ke BotConnector Responses API.",
221
242
  "Claude Code is connected to the BotConnector Anthropic Messages compatibility API.": "Claude Code terhubung ke BotConnector Anthropic Messages compatibility API.",
243
+ 'OpenClaw already has a provider "{id}" with a different endpoint ({base}). BCCLI will not overwrite it; disconnect or change that provider first.': 'OpenClaw sudah memiliki provider "{id}" dengan endpoint lain ({base}). BCCLI tidak akan menimpanya; disconnect/ubah provider tersebut lebih dulu.',
244
+ "The OpenClaw config is not valid JSON: {path}": "Config OpenClaw tidak valid JSON: {path}",
245
+ "OpenClaw is connected to BotConnector ({n} models; default {model}).": "OpenClaw terhubung ke BotConnector ({n} model; default {model}).",
246
+ "API key: env {env} (SecretRef, not stored in the config).": "API key: env {env} (SecretRef, tidak disimpan di config).",
247
+ "Set the default model in OpenClaw with: openclaw models set {provider}/{model}": "Atur model default di OpenClaw dengan: openclaw models set {provider}/{model}",
248
+ "The Hermes config is not valid YAML: {path}": "Config Hermes tidak valid YAML: {path}",
249
+ 'Hermes already has a provider "{id}" with a different endpoint ({base}). BCCLI will not overwrite it; disconnect or change that provider first.': 'Hermes sudah memiliki provider "{id}" dengan endpoint lain ({base}). BCCLI tidak akan menimpanya; disconnect/ubah provider tersebut lebih dulu.',
250
+ "Hermes is connected to BotConnector ({n} models; default {model}).": "Hermes terhubung ke BotConnector ({n} model; default {model}).",
251
+ "API key: env {env} only (via key_env; a plaintext key in config.yaml is never written).": "API key: hanya env {env} (melalui key_env; key plaintext tidak pernah ditulis ke config.yaml).",
222
252
  "{target}: not managed by BCCLI yet.": "{target}: belum dikelola oleh BCCLI.",
223
253
  "{target}: the BotConnector integration was removed and the config restored.": "{target}: integrasi BotConnector dilepas dan config dipulihkan.",
224
254
  "Custom commands:": "Perintah custom:",
@@ -305,7 +335,9 @@ Opsi:
305
335
  "{n} characters": "{n} karakter",
306
336
  "{input} tokens in \xB7 {output} tokens out": "{input} token masuk \xB7 {output} token keluar",
307
337
  "Run: dsh web": "Jalankan: dsh web",
308
- "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."
309
341
  };
310
342
 
311
343
  // src/i18n.ts
@@ -337,108 +369,11 @@ function resolveLanguage(argv, env, fromConfig) {
337
369
  return langFromArgv(argv) ?? parseLang(env.BCCLI_LANG) ?? parseLang(fromConfig) ?? "en";
338
370
  }
339
371
 
340
- // src/config.ts
341
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
342
- import { homedir } from "os";
343
- import { join } from "path";
344
- var ConfigError = class extends Error {
345
- };
346
- var DEFAULT_CONFIG = {
347
- model: "bc-cloud/glm-5.3-flash",
348
- permissionMode: "default",
349
- reasoning: "auto",
350
- providers: Object.fromEntries(PRESETS.map((p) => [p.id, { baseURL: p.baseURL, apiKeyEnv: p.apiKeyEnv }])),
351
- allow: []
352
- };
353
- function bccliHome(env = process.env) {
354
- return env.BCCLI_HOME ?? join(homedir(), ".bccli");
355
- }
356
- function readJsonConfig(path) {
357
- if (!existsSync(path)) return {};
358
- try {
359
- return JSON.parse(readFileSync(path, "utf8"));
360
- } catch (error) {
361
- throw new ConfigError(t("Invalid config: {path} ({error})", { path, error: error.message }));
362
- }
363
- }
364
- function untrustedProviders(project, known) {
365
- const out = {};
366
- for (const [id, provider] of Object.entries(project.providers ?? {})) {
367
- if (known[id] || typeof provider?.baseURL !== "string") continue;
368
- out[id] = { baseURL: provider.baseURL };
369
- }
370
- return out;
371
- }
372
- function loadConfig(cwd, env = process.env) {
373
- const global = readJsonConfig(join(bccliHome(env), "config.json"));
374
- const project = readJsonConfig(join(cwd, ".bccli", "config.json"));
375
- const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
376
- const fromProject = untrustedProviders(project, known);
377
- return {
378
- model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
379
- permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
380
- reasoning: global.reasoning ?? DEFAULT_CONFIG.reasoning,
381
- providers: { ...known, ...fromProject },
382
- projectProviders: Object.keys(fromProject),
383
- allow: [...global.allow ?? []]
384
- };
385
- }
386
- function readCredentials(env = process.env) {
387
- const path = join(bccliHome(env), "credentials");
388
- if (!existsSync(path)) return {};
389
- try {
390
- return JSON.parse(readFileSync(path, "utf8"));
391
- } catch {
392
- return {};
393
- }
394
- }
395
- function saveCredential(providerId, key, env = process.env) {
396
- const home = bccliHome(env);
397
- mkdirSync(home, { recursive: true });
398
- const path = join(home, "credentials");
399
- writeFileSync(path, JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), { mode: 384 });
400
- chmodSync(path, 384);
401
- }
402
- function resolveModel(config, modelRef, env = process.env) {
403
- const slash = modelRef.indexOf("/");
404
- if (slash <= 0 || slash === modelRef.length - 1) {
405
- throw new ConfigError(t("Model must be in provider/model format, e.g. bc-cloud/glm-5.3-flash (got: {ref})", { ref: modelRef }));
406
- }
407
- const providerId = modelRef.slice(0, slash);
408
- const provider = config.providers[providerId];
409
- if (!provider) {
410
- throw new ConfigError(t('Provider "{id}" is not in the config. Available: {list}', { id: providerId, list: Object.keys(config.providers).join(", ") }));
411
- }
412
- const apiKey = (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[providerId] || void 0;
413
- if (provider.apiKeyEnv && !apiKey) {
414
- throw new ConfigError(t("No API key for {id} yet. Run `bccli login` or set the env var {env}.", { id: providerId, env: provider.apiKeyEnv }));
415
- }
416
- return { providerId, model: modelRef.slice(slash + 1), baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey };
417
- }
418
- function removeCredential(providerId, env = process.env) {
419
- const creds = readCredentials(env);
420
- if (!(providerId in creds)) return false;
421
- delete creds[providerId];
422
- const path = join(bccliHome(env), "credentials");
423
- writeFileSync(path, JSON.stringify(creds, null, 2), { mode: 384 });
424
- chmodSync(path, 384);
425
- return true;
426
- }
427
-
428
372
  export {
429
- PRESETS,
430
373
  parseLang,
431
374
  setLanguage,
432
375
  getLanguage,
433
376
  locale,
434
377
  t,
435
- resolveLanguage,
436
- ConfigError,
437
- bccliHome,
438
- readJsonConfig,
439
- loadConfig,
440
- readCredentials,
441
- saveCredential,
442
- resolveModel,
443
- removeCredential
378
+ resolveLanguage
444
379
  };