@botconnector/bccli 0.2.0-beta.1 → 0.3.0-beta.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/README.md CHANGED
@@ -25,25 +25,70 @@ Shift+Tab ganti mode izin: `default` → `acceptEdits` → `plan` → `allowAll`
25
25
  Mode `-p` tidak pernah bertanya: alat yang butuh izin ditolak, kecuali memakai
26
26
  `--allow-all` atau `--allowed-tools bash,edit,fetch`.
27
27
 
28
- ## Provider lain (OpenAI-compatible)
29
-
30
- `~/.bccli/config.json`:
31
-
32
- ```json
33
- {
34
- "model": "openrouter/qwen/qwen3-coder",
35
- "providers": {
36
- "openrouter": { "baseURL": "https://openrouter.ai/api/v1", "apiKeyEnv": "OPENROUTER_API_KEY" },
37
- "local": { "baseURL": "http://127.0.0.1:11434/v1" }
38
- },
39
- "allow": ["bash(npm test)"]
40
- }
41
- ```
28
+ ## Provider
29
+
30
+ `/provider` di dalam sesi (atau `bccli provider add <id>`) memasang provider hanya dengan API key:
31
+
32
+ | Id | Provider |
33
+ |---|---|
34
+ | `bc-cloud` | BotConnector Cloud (default) |
35
+ | `openrouter` | OpenRouter |
36
+ | `openai` | OpenAI |
37
+ | `gemini` | Google Gemini |
38
+ | `deepseek` | DeepSeek |
39
+ | `groq` | Groq |
40
+ | `ollama-cloud` | Ollama Cloud |
41
+ | `ollama` | Ollama lokal (tanpa key) |
42
+ | `lmstudio` | LM Studio lokal (tanpa key) |
43
+
44
+ Provider lain yang OpenAI-compatible: pilih **Custom…** di `/provider`, atau
45
+ `bccli provider add corp --url https://corp.example/v1 --name "Corp"`.
46
+
47
+ `/model` menampilkan model dari **semua** provider yang sudah punya key, dikelompokkan
48
+ per provider; ketik untuk menyaring. Pilihan tersimpan sebagai default.
42
49
 
43
50
  `.bccli/config.json` di dalam project hanya boleh memilih `model` dan menambah provider baru tanpa API key —
44
51
  izin (`permissionMode`, `allow`) dan provider yang sudah ada hanya diatur dari `~/.bccli/config.json`,
45
52
  supaya repo yang kamu clone tidak bisa memberi dirinya izin penuh atau mencuri API key.
46
53
 
54
+ ## MCP
55
+
56
+ `/mcp` (atau `bccli mcp add <nama>`) memasang server dari katalog:
57
+ `playwright`, `context7`, `fetch`, `filesystem`, `git`, `github`. Server remote lain:
58
+ `bccli mcp add nama --url https://…`. Tersimpan di `~/.bccli/mcp.json` (format `mcpServers`
59
+ seperti Claude Code). Semua alat MCP selalu minta izin.
60
+
61
+ `.bccli/mcp.json` di dalam repo tidak dijalankan sebelum kamu setujui (ditanya sekali per folder).
62
+
63
+ ## Subagent
64
+
65
+ Agent bisa mendelegasikan pekerjaan lewat alat `task`:
66
+ - `explore` — hanya baca & cari, bisa jalan paralel;
67
+ - `general` — semua alat (edit/perintah tetap minta izin);
68
+ - custom — `.bccli/agents/nama.md` atau `.claude/agents/nama.md`:
69
+
70
+ ```markdown
71
+ ---
72
+ name: reviewer
73
+ description: review perubahan kode
74
+ tools: [read, grep, glob]
75
+ model: openrouter/qwen/qwen3-coder
76
+ ---
77
+ Kamu reviewer kode. Laporkan bug nyata saja.
78
+ ```
79
+
80
+ ## Skill & perintah custom
81
+
82
+ - Skill: folder berisi `SKILL.md` di `~/.bccli/skills/`, `.bccli/skills/`, `~/.claude/skills/`,
83
+ `.claude/skills/` — dipakai agent otomatis bila cocok, atau `/nama-skill`.
84
+ - Perintah: `commands/nama.md` di folder yang sama → `/nama argumen` (`$ARGUMENTS` diganti argumen).
85
+
86
+ ## Todo & mode plan
87
+
88
+ Tugas ≥ 3 langkah menampilkan checklist di atas kotak input. Di mode plan (Shift+Tab),
89
+ agent hanya membaca lalu mengajukan rencana; `[a]` setujui dengan edit otomatis,
90
+ `[y]` setujui dengan tanya tiap langkah, `[n]` minta perbaikan.
91
+
47
92
  Instruksi project dibaca dari `AGENTS.md` dan `BCCLI.md`.
48
93
 
49
94
  ## Pengembangan
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ HELP_TEXT,
4
+ parseCliArgs
5
+ } from "./chunk-RDQ7HPWS.js";
6
+ import "./chunk-5XN44Q55.js";
7
+ import "./chunk-Z5JM76EL.js";
8
+ export {
9
+ HELP_TEXT,
10
+ parseCliArgs
11
+ };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PRESETS,
4
+ bccliHome,
5
+ readCredentials,
6
+ readJsonConfig,
7
+ removeCredential
8
+ } from "./chunk-Z5JM76EL.js";
9
+
10
+ // src/providers.ts
11
+ import { mkdirSync, writeFileSync } from "fs";
12
+ import { join } from "path";
13
+ function providerName(config, id) {
14
+ return PRESETS.find((p) => p.id === id)?.name ?? config.providers[id]?.name ?? id;
15
+ }
16
+ function hasKey(config, id, env = process.env) {
17
+ const provider = config.providers[id];
18
+ if (!provider) return false;
19
+ if (!provider.apiKeyEnv) return true;
20
+ return !!env[provider.apiKeyEnv] || !!readCredentials(env)[id];
21
+ }
22
+ var globalPath = (env) => join(bccliHome(env), "config.json");
23
+ function readGlobalConfigFile(env = process.env) {
24
+ return readJsonConfig(globalPath(env));
25
+ }
26
+ function save(data, env) {
27
+ mkdirSync(bccliHome(env), { recursive: true });
28
+ writeFileSync(globalPath(env), `${JSON.stringify(data, null, 2)}
29
+ `);
30
+ }
31
+ function writeGlobalConfig(patch, env = process.env) {
32
+ const current = readGlobalConfigFile(env);
33
+ const next = { ...current, ...patch };
34
+ if (patch.providers) next.providers = { ...current.providers, ...patch.providers };
35
+ save(next, env);
36
+ }
37
+ function removeGlobalProvider(id, env = process.env) {
38
+ removeCredential(id, env);
39
+ const current = readGlobalConfigFile(env);
40
+ if (!current.providers?.[id]) return;
41
+ const { [id]: _removed, ...providers } = current.providers;
42
+ save({ ...current, providers }, env);
43
+ }
44
+
45
+ export {
46
+ providerName,
47
+ hasKey,
48
+ writeGlobalConfig,
49
+ removeGlobalProvider
50
+ };
@@ -1,58 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/print.ts
4
- function toolLabel(tool) {
5
- return tool.charAt(0).toUpperCase() + tool.slice(1);
6
- }
7
- async function runPrint(rt, prompt, io = { out: process.stdout, err: process.stderr }) {
8
- let failed = false;
9
- let wroteText = false;
10
- rt.agent.onEvent = (event) => {
11
- switch (event.type) {
12
- case "text":
13
- io.out.write(event.delta);
14
- wroteText = true;
15
- break;
16
- case "toolStart":
17
- io.err.write(`\u23BF ${toolLabel(event.tool)} ${event.target}
18
- `);
19
- break;
20
- case "toolEnd":
21
- if (event.isError) io.err.write(` \u2717 ${event.output.split("\n")[0]}
22
- `);
23
- break;
24
- case "stepLimit":
25
- io.err.write("Batas langkah tercapai.\n");
26
- failed = true;
27
- break;
28
- case "aborted":
29
- io.err.write("Dibatalkan.\n");
30
- failed = true;
31
- break;
32
- case "error":
33
- io.err.write(`Error: ${event.message}
34
- `);
35
- failed = true;
36
- break;
37
- }
38
- };
39
- rt.agent.askPermission = async (req) => {
40
- io.err.write(` Butuh izin untuk ${req.tool}. Jalankan dengan --allow-all atau --allowed-tools ${req.kind}.
41
- `);
42
- return "no";
43
- };
44
- const controller = new AbortController();
45
- const onSigint = () => controller.abort();
46
- process.once("SIGINT", onSigint);
47
- try {
48
- await rt.agent.run(prompt, controller.signal);
49
- } finally {
50
- process.off("SIGINT", onSigint);
51
- }
52
- if (wroteText) io.out.write("\n");
53
- return failed ? 1 : 0;
54
- }
55
-
56
3
  // src/permissions.ts
57
4
  import { isAbsolute, relative, resolve, sep } from "path";
58
5
  var MODE_ORDER = ["default", "acceptEdits", "plan", "allowAll"];
@@ -95,6 +42,7 @@ var Permissions = class {
95
42
  if (!segments.length) return void 0;
96
43
  return [...new Set(segments.map((s) => `bash(${commandKey(s)})`))];
97
44
  }
45
+ if (req.kind === "mcp") return [`mcp(${req.tool})`];
98
46
  return [];
99
47
  }
100
48
  check(req) {
@@ -107,36 +55,16 @@ var Permissions = class {
107
55
  if (req.kind === "edit" && this.mode === "acceptEdits") return "allow";
108
56
  return needed.every((rule) => this.rules.has(rule)) ? "allow" : "ask";
109
57
  }
58
+ revoke(rule) {
59
+ this.rules.delete(rule);
60
+ }
110
61
  allowForSession(req) {
111
62
  for (const rule of this.rulesFor(req) ?? []) this.rules.add(rule);
112
63
  }
113
64
  };
114
65
 
115
- // src/tools/ignore.ts
116
- import { readFileSync } from "fs";
117
- import { join } from "path";
118
- function ignorePatterns(root) {
119
- const patterns = ["**/node_modules/**", "**/.git/**"];
120
- let lines = [];
121
- try {
122
- lines = readFileSync(join(root, ".gitignore"), "utf8").split(/\r?\n/);
123
- } catch {
124
- return patterns;
125
- }
126
- for (const raw of lines) {
127
- const line = raw.trim();
128
- if (!line || line.startsWith("#") || line.startsWith("!")) continue;
129
- const p = line.replace(/^\//, "").replace(/\/$/, "");
130
- patterns.push(`**/${p}`, `**/${p}/**`);
131
- }
132
- return patterns;
133
- }
134
-
135
66
  export {
136
67
  MODE_ORDER,
137
68
  nextMode,
138
- Permissions,
139
- toolLabel,
140
- runPrint,
141
- ignorePatterns
69
+ Permissions
142
70
  };
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/login.ts
4
+ function readSecret(prompt) {
5
+ return new Promise((resolve) => {
6
+ process.stdout.write(prompt);
7
+ const stdin = process.stdin;
8
+ if (!stdin.isTTY) {
9
+ let data = "";
10
+ stdin.on("data", (d) => {
11
+ data += d;
12
+ });
13
+ stdin.on("end", () => resolve(data.trim()));
14
+ return;
15
+ }
16
+ let value = "";
17
+ stdin.setRawMode(true);
18
+ stdin.resume();
19
+ const onData = (buffer) => {
20
+ for (const ch of buffer.toString()) {
21
+ if (ch === "\r" || ch === "\n") {
22
+ stdin.setRawMode(false);
23
+ stdin.pause();
24
+ stdin.off("data", onData);
25
+ process.stdout.write("\n");
26
+ resolve(value.trim());
27
+ return;
28
+ }
29
+ if (ch === "") process.exit(130);
30
+ if (ch === "\x7F" || ch === "\b") value = value.slice(0, -1);
31
+ else value += ch;
32
+ }
33
+ };
34
+ stdin.on("data", onData);
35
+ });
36
+ }
37
+ async function runLogin(providerId, cwd) {
38
+ const { runProviderCommand } = await import("./providerCli-7QNAYIFT.js");
39
+ const { parseCliArgs } = await import("./args-EVIFNGPQ.js");
40
+ return runProviderCommand(parseCliArgs(["provider", "add", providerId]), {
41
+ env: process.env,
42
+ cwd,
43
+ out: (s) => console.log(s),
44
+ err: (s) => console.error(s),
45
+ readSecret
46
+ });
47
+ }
48
+
49
+ export {
50
+ readSecret,
51
+ runLogin
52
+ };
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ hasKey,
4
+ providerName,
5
+ removeGlobalProvider,
6
+ writeGlobalConfig
7
+ } from "./chunk-5C6NQFSI.js";
8
+ import {
9
+ PRESETS,
10
+ loadConfig,
11
+ readCredentials,
12
+ saveCredential
13
+ } from "./chunk-Z5JM76EL.js";
14
+
15
+ // src/provider.ts
16
+ var ProviderError = class extends Error {
17
+ constructor(message, status) {
18
+ super(message);
19
+ this.status = status;
20
+ }
21
+ status;
22
+ };
23
+ var retryable = (status) => status === 429 || status >= 500;
24
+ function sleep(ms, signal) {
25
+ return new Promise((resolve, reject) => {
26
+ const timer = setTimeout(resolve, ms);
27
+ signal?.addEventListener(
28
+ "abort",
29
+ () => {
30
+ clearTimeout(timer);
31
+ reject(signal.reason);
32
+ },
33
+ { once: true }
34
+ );
35
+ });
36
+ }
37
+ function errorMessage(body) {
38
+ try {
39
+ const parsed = JSON.parse(body);
40
+ return parsed?.error?.message ?? parsed?.message ?? body.slice(0, 300);
41
+ } catch {
42
+ return body.slice(0, 300);
43
+ }
44
+ }
45
+ var toUsage = (u) => u ? { inputTokens: u.prompt_tokens ?? 0, outputTokens: u.completion_tokens ?? 0 } : void 0;
46
+ async function readStream(res, onText) {
47
+ let text = "";
48
+ let usage;
49
+ let finishReason;
50
+ const calls = [];
51
+ const byIndex = /* @__PURE__ */ new Map();
52
+ let current;
53
+ const decoder = new TextDecoder();
54
+ let buffer = "";
55
+ const handle = (line) => {
56
+ if (!line.startsWith("data:")) return;
57
+ const data = line.slice(5).trim();
58
+ if (!data || data === "[DONE]") return;
59
+ let event;
60
+ try {
61
+ event = JSON.parse(data);
62
+ } catch {
63
+ return;
64
+ }
65
+ if (event.error) throw new ProviderError(`Error dari provider: ${event.error.message ?? JSON.stringify(event.error)}`);
66
+ if (event.usage) usage = toUsage(event.usage);
67
+ const choice = event.choices?.[0];
68
+ if (!choice) return;
69
+ if (choice.finish_reason) finishReason = choice.finish_reason;
70
+ const delta = choice.delta ?? {};
71
+ if (delta.content) {
72
+ text += delta.content;
73
+ onText?.(delta.content);
74
+ }
75
+ for (const tc of delta.tool_calls ?? []) {
76
+ let target = tc.index !== void 0 ? byIndex.get(tc.index) : current;
77
+ if (!target || tc.index === void 0 && tc.id && target.id && target.id !== tc.id) {
78
+ target = { id: "", name: "", arguments: "" };
79
+ calls.push(target);
80
+ if (tc.index !== void 0) byIndex.set(tc.index, target);
81
+ }
82
+ current = target;
83
+ if (tc.id) target.id = tc.id;
84
+ if (tc.function?.name && !target.name) target.name = tc.function.name;
85
+ if (tc.function?.arguments) target.arguments += tc.function.arguments;
86
+ }
87
+ };
88
+ for await (const chunk of res.body) {
89
+ buffer += decoder.decode(chunk, { stream: true });
90
+ let newline = buffer.indexOf("\n");
91
+ while (newline >= 0) {
92
+ handle(buffer.slice(0, newline).trim());
93
+ buffer = buffer.slice(newline + 1);
94
+ newline = buffer.indexOf("\n");
95
+ }
96
+ }
97
+ handle(buffer.trim());
98
+ const toolCalls = calls.map((call, index) => ({ ...call, id: call.id || `call_${index}` }));
99
+ return { text, toolCalls, usage, finishReason };
100
+ }
101
+ async function readJson(res) {
102
+ const body = await res.json();
103
+ const choice = body.choices?.[0];
104
+ const toolCalls = (choice?.message?.tool_calls ?? []).map((tc, i) => ({
105
+ id: tc.id || `call_${i}`,
106
+ name: tc.function?.name ?? "",
107
+ arguments: tc.function?.arguments ?? ""
108
+ }));
109
+ return { text: choice?.message?.content ?? "", toolCalls, usage: toUsage(body.usage), finishReason: choice?.finish_reason };
110
+ }
111
+ function createProvider({ baseURL, apiKey, model, fetch: doFetch = fetch, retryDelayMs = 1e3 }) {
112
+ const headers = { "content-type": "application/json" };
113
+ if (apiKey) headers.authorization = `Bearer ${apiKey}`;
114
+ async function post(path, body, signal) {
115
+ let lastError;
116
+ for (let attempt = 0; attempt < 4; attempt++) {
117
+ if (attempt > 0) await sleep(retryDelayMs * 2 ** (attempt - 1), signal);
118
+ let res;
119
+ try {
120
+ res = await doFetch(`${baseURL}${path}`, { method: "POST", headers, body: JSON.stringify(body), signal });
121
+ } catch (error) {
122
+ if (signal?.aborted) throw error;
123
+ lastError = new ProviderError(`Tidak bisa terhubung ke ${baseURL}: ${error.message}`);
124
+ continue;
125
+ }
126
+ if (res.ok) return res;
127
+ const text = await res.text().catch(() => "");
128
+ lastError = new ProviderError(`${res.status} dari ${baseURL}: ${errorMessage(text)}`, res.status);
129
+ if (!retryable(res.status)) throw lastError;
130
+ }
131
+ throw lastError ?? new ProviderError("Permintaan gagal");
132
+ }
133
+ return {
134
+ async chat({ messages, tools, signal, onText }) {
135
+ const res = await post(
136
+ "/chat/completions",
137
+ { model, messages, stream: true, stream_options: { include_usage: true }, ...tools?.length ? { tools } : {} },
138
+ signal
139
+ );
140
+ if ((res.headers.get("content-type") ?? "").includes("application/json")) return readJson(res);
141
+ if (!res.body) throw new ProviderError("Respons provider kosong");
142
+ return readStream(res, onText);
143
+ },
144
+ async listModels() {
145
+ const res = await doFetch(`${baseURL}/models`, { headers });
146
+ if (!res.ok) throw new ProviderError(`${res.status} saat mengambil daftar model`, res.status);
147
+ const body = await res.json();
148
+ return (body.data ?? []).map((m) => m.id).sort();
149
+ }
150
+ };
151
+ }
152
+
153
+ // src/models.ts
154
+ function order(ids) {
155
+ const presetIds = PRESETS.map((p) => p.id);
156
+ const presets = presetIds.filter((id) => ids.includes(id));
157
+ const custom = ids.filter((id) => !presetIds.includes(id)).sort();
158
+ return [...presets, ...custom];
159
+ }
160
+ async function listAllModels(config, env = process.env, opts = {}) {
161
+ const timeoutMs = opts.timeoutMs ?? 5e3;
162
+ const baseFetch = opts.fetch ?? fetch;
163
+ const untrusted = new Set(config.projectProviders ?? []);
164
+ const ids = order(Object.keys(config.providers)).filter((id) => opts.only ? id === opts.only : !untrusted.has(id) && hasKey(config, id, env));
165
+ const results = await Promise.all(
166
+ ids.map(async (id) => {
167
+ const provider = config.providers[id];
168
+ const apiKey = (opts.keyOverride && id === opts.only ? opts.keyOverride : void 0) || (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[id] || void 0;
169
+ const local = !provider.apiKeyEnv;
170
+ const timedFetch = ((input, init) => baseFetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
171
+ const list = createProvider({ baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey, model: "", fetch: timedFetch }).listModels();
172
+ let timer;
173
+ const timeout = new Promise((_, reject) => {
174
+ timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
175
+ });
176
+ try {
177
+ const models = await Promise.race([list, timeout]);
178
+ return { providerId: id, providerName: providerName(config, id), models };
179
+ } catch {
180
+ if (local && !opts.only) return void 0;
181
+ return { providerId: id, providerName: providerName(config, id), models: [], error: "tidak bisa dihubungi" };
182
+ } finally {
183
+ clearTimeout(timer);
184
+ }
185
+ })
186
+ );
187
+ return results.filter((g) => !!g);
188
+ }
189
+
190
+ // src/providerCli.ts
191
+ async function addProviderKey(providerId, key, deps) {
192
+ const config = loadConfig(deps.cwd, deps.env);
193
+ const [group] = await listAllModels(config, deps.env, { only: providerId, fetch: deps.fetch, keyOverride: key || void 0 });
194
+ if (!group || group.error) return { ok: false, error: `API key ditolak atau ${providerName(config, providerId)} tidak bisa dihubungi.` };
195
+ if (key) saveCredential(providerId, key, deps.env);
196
+ return { ok: true, models: group.models.length };
197
+ }
198
+ async function runProviderCommand(args, deps) {
199
+ const [action = "list", id] = args.subArgs;
200
+ const config = loadConfig(deps.cwd, deps.env);
201
+ if (action === "list") {
202
+ for (const pid of Object.keys(config.providers)) {
203
+ const mark = hasKey(config, pid, deps.env) ? "\u2713" : "\u25CB";
204
+ deps.out(`${mark} ${pid.padEnd(14)} ${providerName(config, pid).padEnd(20)} ${config.providers[pid].baseURL}`);
205
+ }
206
+ return 0;
207
+ }
208
+ if (!id) {
209
+ deps.err(`Pemakaian: bccli provider ${action} <id>`);
210
+ return 1;
211
+ }
212
+ if (action === "remove") {
213
+ removeGlobalProvider(id, deps.env);
214
+ deps.out(`${id} dihapus.`);
215
+ return 0;
216
+ }
217
+ if (action !== "add") {
218
+ deps.err(`Aksi tidak dikenal: ${action}. Pakai list, add, atau remove.`);
219
+ return 1;
220
+ }
221
+ const preset = PRESETS.find((p) => p.id === id);
222
+ if (!preset && !config.providers[id]) {
223
+ if (!args.url) {
224
+ deps.err(`${id} bukan preset. Untuk provider custom tambahkan --url <base URL OpenAI-compatible>.`);
225
+ return 1;
226
+ }
227
+ writeGlobalConfig(
228
+ { providers: { [id]: { baseURL: args.url, ...args.name ? { name: args.name } : {}, ...args.keyEnv ? { apiKeyEnv: args.keyEnv } : {} } } },
229
+ deps.env
230
+ );
231
+ }
232
+ const needsKey = !!loadConfig(deps.cwd, deps.env).providers[id].apiKeyEnv || !preset;
233
+ const key = needsKey ? await deps.readSecret(`API key untuk ${id} (kosongkan bila tidak perlu): `) : "";
234
+ const result = await addProviderKey(id, key, deps);
235
+ if (!result.ok) {
236
+ deps.err(result.error);
237
+ return 1;
238
+ }
239
+ deps.out(`${id} siap. ${result.models} model tersedia.`);
240
+ return 0;
241
+ }
242
+
243
+ export {
244
+ createProvider,
245
+ listAllModels,
246
+ addProviderKey,
247
+ runProviderCommand
248
+ };
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ MODE_ORDER
4
+ } from "./chunk-5XN44Q55.js";
5
+ import {
6
+ ConfigError
7
+ } from "./chunk-Z5JM76EL.js";
8
+
9
+ // src/args.ts
10
+ import { parseArgs } from "util";
11
+ var HELP_TEXT = `BCCLI \u2014 agent AI BotConnector di terminal
12
+
13
+ Pemakaian:
14
+ bccli [tugas] mode interaktif (opsional langsung dengan tugas)
15
+ bccli -p "tugas" jalankan satu tugas tanpa interaksi (skrip/CI)
16
+ bccli login [provider] simpan API key (default: bc-cloud)
17
+ bccli models daftar model dari provider aktif
18
+ bccli provider list|add <id>|remove <id> kelola provider (custom: --url <url> [--name N] [--key-env ENV])
19
+ bccli mcp list|add <nama>|remove <nama> kelola server MCP (katalog, atau --url <url>)
20
+
21
+ Opsi:
22
+ -m, --model <provider/model> pilih model, contoh bc-cloud/glm-5.3-flash
23
+ -c, --continue lanjutkan sesi terakhir di folder ini
24
+ -r, --resume pilih sesi untuk dilanjutkan
25
+ --allow-all jalankan semua alat tanpa minta izin
26
+ --allowed-tools <a,b> alat yang boleh tanpa izin: bash, edit, fetch
27
+ --permission-mode <mode> default | acceptEdits | plan | allowAll
28
+ -v, --version versi
29
+ -h, --help bantuan ini`;
30
+ function parseCliArgs(argv) {
31
+ const { values, positionals } = parseArgs({
32
+ args: argv,
33
+ allowPositionals: true,
34
+ options: {
35
+ print: { type: "boolean", short: "p" },
36
+ model: { type: "string", short: "m" },
37
+ continue: { type: "boolean", short: "c" },
38
+ resume: { type: "boolean", short: "r" },
39
+ "allow-all": { type: "boolean" },
40
+ "allowed-tools": { type: "string" },
41
+ "permission-mode": { type: "string" },
42
+ help: { type: "boolean", short: "h" },
43
+ version: { type: "boolean", short: "v" },
44
+ url: { type: "string" },
45
+ name: { type: "string" },
46
+ "key-env": { type: "string" },
47
+ value: { type: "string", multiple: true }
48
+ }
49
+ });
50
+ const mode = values["permission-mode"];
51
+ if (mode && !MODE_ORDER.includes(mode)) {
52
+ throw new ConfigError(`--permission-mode harus salah satu dari: ${MODE_ORDER.join(", ")}`);
53
+ }
54
+ const [first, ...rest] = positionals;
55
+ const SUBCOMMANDS = ["login", "models", "provider", "mcp"];
56
+ const command = SUBCOMMANDS.includes(first) ? first : "run";
57
+ const words = command === "run" ? positionals : rest;
58
+ return {
59
+ command,
60
+ prompt: command === "run" && words.length ? words.join(" ") : void 0,
61
+ print: !!values.print,
62
+ model: values.model,
63
+ continue: !!values.continue,
64
+ resume: !!values.resume,
65
+ allowAll: !!values["allow-all"],
66
+ allowedTools: (values["allowed-tools"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
67
+ permissionMode: mode,
68
+ help: !!values.help,
69
+ version: !!values.version,
70
+ loginProvider: command === "login" ? rest[0] ?? "bc-cloud" : void 0,
71
+ subArgs: command === "provider" || command === "mcp" ? rest : [],
72
+ url: values.url,
73
+ name: values.name,
74
+ keyEnv: values["key-env"],
75
+ values: values.value ?? []
76
+ };
77
+ }
78
+
79
+ export {
80
+ HELP_TEXT,
81
+ parseCliArgs
82
+ };