@botconnector/bccli 0.4.0-beta.4 → 0.4.0-beta.41
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 +80 -1
- package/dist/acp-SZWZ5EE4.js +114 -0
- package/dist/args-4SMOHFN5.js +13 -0
- package/dist/chunk-3HCOAJEQ.js +190 -0
- package/dist/chunk-AS7VV7D3.js +195 -0
- package/dist/{chunk-NGLKANMD.js → chunk-AVB6M4JE.js} +24 -2
- package/dist/chunk-BUHWQU3V.js +231 -0
- package/dist/chunk-DBHDZ5EP.js +723 -0
- package/dist/{chunk-GW36UMHZ.js → chunk-DSFHN5CH.js} +2 -2
- package/dist/chunk-EQQTTE2K.js +8 -0
- package/dist/{chunk-ELML4KVQ.js → chunk-GAFJEQTJ.js} +32 -2
- package/dist/chunk-GGNXHJL6.js +884 -0
- package/dist/chunk-HE33XAPZ.js +341 -0
- package/dist/{chunk-UESQNU4A.js → chunk-ISCT3PN3.js} +14 -9
- package/dist/chunk-JROUQ5YS.js +76 -0
- package/dist/chunk-LN32JIZR.js +56 -0
- package/dist/chunk-M23QBX3V.js +419 -0
- package/dist/chunk-M4IYXI3V.js +2125 -0
- package/dist/chunk-M7LLJQ7M.js +310 -0
- package/dist/chunk-NI5JZUZ3.js +63 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/{chunk-U7DLMVPZ.js → chunk-SESBH44L.js} +2 -1
- package/dist/{chunk-SJH3YLW4.js → chunk-VM7HYAJS.js} +4 -1
- package/dist/cli.js +94 -1278
- package/dist/{config-NPSYFAFD.js → config-C4KLLDC6.js} +5 -2
- package/dist/integrationCli-HR7CDUSI.js +685 -0
- package/dist/{login-F4G5QXOU.js → login-T5S6SPZA.js} +1 -1
- package/dist/{mcpCli-Q2H63ISP.js → mcpCli-5QTXH7XJ.js} +42 -9
- package/dist/modelsCli-DKTT3UZD.js +36 -0
- package/dist/providerCli-DDH4ZZUY.js +15 -0
- package/dist/sdk.js +5179 -0
- package/dist/serve-I2VQUTHR.js +178 -0
- package/dist/serve-KQ35P4W3.js +514 -0
- package/dist/session-H2YL5RSB.js +12 -0
- package/dist/setup-45WQXUBK.js +23 -0
- package/dist/ui-S4QFJJIB.js +2280 -0
- package/dist/update-VBDEHHMR.js +109 -0
- package/package.json +24 -3
- package/dist/args-FCLPQ7TO.js +0 -11
- package/dist/chunk-C5AFWX7H.js +0 -328
- package/dist/chunk-OSWQPSA4.js +0 -252
- package/dist/chunk-RZUJYAQV.js +0 -112
- package/dist/chunk-UN7QXPCB.js +0 -82
- package/dist/providerCli-MXV6PD6F.js +0 -11
- package/dist/session-7UB77CEG.js +0 -9
- package/dist/ui-7FXPXXGN.js +0 -1333
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Agent AI coding dari BotConnector yang bekerja di terminal: membaca kode, menged
|
|
|
4
4
|
dan menjalankan perintah — dengan izin kamu.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
npm i -g @botconnector/bccli@next #
|
|
7
|
+
npm i -g @botconnector/bccli@next # beta terbaru, butuh Node.js 22+
|
|
8
8
|
bccli login # simpan API key BotConnector Cloud
|
|
9
9
|
cd project-kamu && bccli
|
|
10
10
|
```
|
|
@@ -15,8 +15,10 @@ 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 |
|
|
21
|
+
| `bccli --reasoning <auto|off|low|medium|high|max>` | pilih level reasoning untuk invocation ini |
|
|
20
22
|
| `bccli models` | daftar model |
|
|
21
23
|
|
|
22
24
|
Perintah di dalam sesi (ketik `/` untuk saran, ↑↓ pilih, Enter jalankan):
|
|
@@ -25,7 +27,9 @@ Perintah di dalam sesi (ketik `/` untuk saran, ↑↓ pilih, Enter jalankan):
|
|
|
25
27
|
|---|---|
|
|
26
28
|
| `/help` | daftar perintah dan pintasan |
|
|
27
29
|
| `/model` | ganti model |
|
|
30
|
+
| `/reasoning` | pilih Auto / Off / Low / Medium / High / Max |
|
|
28
31
|
| `/provider` | tambah/pilih provider AI |
|
|
32
|
+
| `/language` | ganti bahasa tampilan (`en` / `id`) |
|
|
29
33
|
| `/mcp` | pasang/kelola server MCP |
|
|
30
34
|
| `/new` | sesi baru (sesi lama tetap tersimpan) |
|
|
31
35
|
| `/resume` | lanjutkan sesi lain di folder ini |
|
|
@@ -54,6 +58,19 @@ Shift+Tab ganti mode izin: `default` → `acceptEdits` → `plan` → `allowAll`
|
|
|
54
58
|
Mode `-p` tidak pernah bertanya: alat yang butuh izin ditolak, kecuali memakai
|
|
55
59
|
`--allow-all` atau `--allowed-tools bash,edit,fetch`.
|
|
56
60
|
|
|
61
|
+
## Bahasa
|
|
62
|
+
|
|
63
|
+
Tampilan BCCLI (bantuan, pesan, prompt izin) memakai **bahasa Inggris secara default**. Bahasa Indonesia tetap tersedia:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bccli --lang id # hanya untuk satu kali jalan
|
|
67
|
+
BCCLI_LANG=id bccli # lewat environment
|
|
68
|
+
/language id # di dalam sesi; disimpan di ~/.bccli/config.json ("language")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Urutan prioritas: `--lang`, lalu `BCCLI_LANG`, lalu `language` di config, lalu Inggris. Bahasa jawaban model mengikuti bahasa yang kamu tulis, dan
|
|
72
|
+
pesan alat yang dibaca model (hasil tool, galat) selalu berbahasa Inggris supaya tidak mengganggu bahasa jawabannya.
|
|
73
|
+
|
|
57
74
|
## Provider
|
|
58
75
|
|
|
59
76
|
`/provider` di dalam sesi (atau `bccli provider add <id>`) memasang provider hanya dengan API key:
|
|
@@ -76,10 +93,64 @@ Provider lain yang OpenAI-compatible: pilih **Custom…** di `/provider`, atau
|
|
|
76
93
|
`/model` menampilkan model dari **semua** provider yang sudah punya key, dikelompokkan
|
|
77
94
|
per provider; ketik untuk menyaring. Pilihan tersimpan sebagai default.
|
|
78
95
|
|
|
96
|
+
Reasoning dapat dipilih lewat `bccli --reasoning <auto|off|low|medium|high|max>` atau `/reasoning`.
|
|
97
|
+
**Auto** memakai default model/provider dan BCCLI tidak mengirim override reasoning apa pun. BCCLI tidak
|
|
98
|
+
otomatis menurunkan atau menaikkan reasoning effort. Level manual diteruskan bila kontrak provider/model
|
|
99
|
+
mendukungnya; level yang tidak didukung menghasilkan error yang jelas dan tidak di-downgrade diam-diam.
|
|
100
|
+
|
|
79
101
|
`.bccli/config.json` di dalam project hanya boleh memilih `model` dan menambah provider baru tanpa API key —
|
|
102
|
+
preference `reasoning` hanya dibaca dari config user `~/.bccli/config.json`, bukan dari repo yang di-clone —
|
|
80
103
|
izin (`permissionMode`, `allow`) dan provider yang sudah ada hanya diatur dari `~/.bccli/config.json`,
|
|
81
104
|
supaya repo yang kamu clone tidak bisa memberi dirinya izin penuh atau mencuri API key.
|
|
82
105
|
|
|
106
|
+
## Integrasi agent eksternal
|
|
107
|
+
|
|
108
|
+
BCCLI dapat memasang BotConnector sebagai provider untuk agent lain:
|
|
109
|
+
|
|
110
|
+
| Perintah | Fungsi |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `bccli integrations` | lihat status integrasi |
|
|
113
|
+
| `bccli connect opencode` | hubungkan OpenCode ke BotConnector |
|
|
114
|
+
| `bccli connect aider` | hubungkan Aider ke BotConnector |
|
|
115
|
+
| `bccli connect cline` | hubungkan Cline CLI ke BotConnector |
|
|
116
|
+
| `bccli connect deepseek-harness` / `bccli connect dsh` | hubungkan DeepSeek Harness ke BotConnector |
|
|
117
|
+
| `bccli connect codex` | buat profile Codex Responses API + launcher BotConnector |
|
|
118
|
+
| `bccli connect claude-code` | buat launcher Claude Code via Anthropic Messages compatibility |
|
|
119
|
+
| `bccli connect cursor` | buat guided setup Cursor + warning override Base URL |
|
|
120
|
+
| `bccli connect openai-cli` | buat launcher OpenAI CLI khusus BotConnector |
|
|
121
|
+
| `bccli connect openai-sdk` | buat env profile untuk OpenAI Python/Node SDK |
|
|
122
|
+
| `bccli connect openai-compatible` | buat env profile universal OpenAI-compatible |
|
|
123
|
+
| `bccli disconnect <agent>` | pulihkan config sebelum integrasi |
|
|
124
|
+
|
|
125
|
+
OpenCode memakai config global `~/.config/opencode/opencode.json`. BCCLI mengambil katalog model BotConnector saat connect,
|
|
126
|
+
menjaga config provider lain, dan menyimpan secret di file terpisah di `~/.bccli/integrations/`.
|
|
127
|
+
Aider memakai `~/.aider.conf.yml` yang menunjuk ke env file BCCLI terpisah; BCCLI menolak menimpa setting
|
|
128
|
+
OpenAI/env-file Aider yang sudah ada.
|
|
129
|
+
|
|
130
|
+
Cline memakai provider `openai-compatible` di `~/.cline/data/settings/providers.json`. Jika slot itu sudah dipakai
|
|
131
|
+
endpoint lain, BCCLI menolak menimpanya. Cline sendiri menyimpan API key provider di file tersebut.
|
|
132
|
+
|
|
133
|
+
DeepSeek Harness memakai custom provider `botconnector` di `$DSH_HOME/settings.yaml` dengan
|
|
134
|
+
`api: openai-completions` dan `apiKeyEnv: BOTCONNECTOR_API_KEY`; key diletakkan pada blok BCCLI di
|
|
135
|
+
`$DSH_HOME/.env`. Provider DSH lain tetap dipertahankan. Setelah connect, jalankan profile web dengan
|
|
136
|
+
`dsh web` (setara dengan `dsh --profile web`); `dsh` tanpa profile memang ditolak oleh DSH 0.1.x.
|
|
137
|
+
|
|
138
|
+
Codex memakai profile `$CODEX_HOME/botconnector.config.toml` dengan `wire_api = "responses"` dan launcher
|
|
139
|
+
BCCLI yang membaca key dari storage BCCLI saat dijalankan. Claude Code memakai launcher terisolasi dengan
|
|
140
|
+
`ANTHROPIC_BASE_URL=https://api.botconnector.id` dan endpoint kompatibilitas `/v1/messages` + token counting.
|
|
141
|
+
Login/config utama Codex dan Claude Code tidak ditimpa.
|
|
142
|
+
|
|
143
|
+
External agent memanggil BotConnector langsung, jadi discovery model tidak memakai identitas khusus BCCLI.
|
|
144
|
+
Akses yang memang dibatasi ke BCCLI/Web App (misalnya Luna launch access) tidak otomatis diwariskan.
|
|
145
|
+
Sebelum mengubah config eksternal, BCCLI membuat backup agar disconnect dapat memulihkannya.
|
|
146
|
+
|
|
147
|
+
## Internet
|
|
148
|
+
|
|
149
|
+
- `web_search`: mencari di web tanpa perlu izin. Memakai akun BotConnector Cloud
|
|
150
|
+
bila ada key, kalau tidak langsung ke Keenable (gratis, tanpa key). Hasilnya
|
|
151
|
+
judul, URL, dan cuplikan singkat, jadi hemat token.
|
|
152
|
+
- `fetch`: membuka satu URL dari komputer ini (butuh izin per host).
|
|
153
|
+
|
|
83
154
|
## MCP
|
|
84
155
|
|
|
85
156
|
`/mcp` (atau `bccli mcp add <nama>`) memasang server dari katalog:
|
|
@@ -89,6 +160,14 @@ seperti Claude Code). Semua alat MCP selalu minta izin.
|
|
|
89
160
|
|
|
90
161
|
`.bccli/mcp.json` di dalam repo tidak dijalankan sebelum kamu setujui (ditanya sekali per folder).
|
|
91
162
|
|
|
163
|
+
`bccli mcp serve` menjalankan MCP server stdio dengan tools BotConnector milikmu
|
|
164
|
+
(`bc_search`, `bc_fetch`, `bc_models`, `bc_chat`) untuk dipakai Claude Code, Cursor,
|
|
165
|
+
opencode, dan host MCP lain. Butuh `bccli login` (atau `BOTCONNECTOR_API_KEY`):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
claude mcp add botconnector -- npx -y @botconnector/bccli mcp serve
|
|
169
|
+
```
|
|
170
|
+
|
|
92
171
|
## Subagent
|
|
93
172
|
|
|
94
173
|
Agent bisa mendelegasikan pekerjaan lewat alat `task`:
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/acp.ts
|
|
4
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
5
|
+
function runAcp(input, output, opts) {
|
|
6
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
7
|
+
let nextSession = 1;
|
|
8
|
+
const write = (msg) => {
|
|
9
|
+
output.write(`${JSON.stringify(msg)}
|
|
10
|
+
`);
|
|
11
|
+
};
|
|
12
|
+
const reply = (id, result) => write({ jsonrpc: "2.0", id, result });
|
|
13
|
+
const replyError = (id, code, message) => write({ jsonrpc: "2.0", id, error: { code, message } });
|
|
14
|
+
const handle = async (msg) => {
|
|
15
|
+
const { id, method, params } = msg ?? {};
|
|
16
|
+
if (method === "initialize") {
|
|
17
|
+
return reply(id, {
|
|
18
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
19
|
+
agentCapabilities: {
|
|
20
|
+
loadSession: false,
|
|
21
|
+
promptCapabilities: { image: false, audio: false, embeddedContext: false }
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (method === "session/new") {
|
|
26
|
+
const sessionId = `s${nextSession++}`;
|
|
27
|
+
sessions.set(sessionId, { controller: null });
|
|
28
|
+
return reply(id, { sessionId });
|
|
29
|
+
}
|
|
30
|
+
if (method === "session/cancel") {
|
|
31
|
+
sessions.get(params?.sessionId ?? "")?.controller?.abort();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (method === "session/prompt") {
|
|
35
|
+
const sid = params?.sessionId ?? "";
|
|
36
|
+
const session = sessions.get(sid);
|
|
37
|
+
if (!session) return replyError(id, -32602, "Unknown session");
|
|
38
|
+
const text = (params?.prompt ?? []).filter((p) => p?.type === "text").map((p) => String(p.text ?? "")).join("\n");
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
session.controller = controller;
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
controller.signal.addEventListener("abort", () => {
|
|
43
|
+
cancelled = true;
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
const agent = opts.newAgent(params?.cwd ?? opts.cwd);
|
|
47
|
+
await agent.run(
|
|
48
|
+
text,
|
|
49
|
+
(chunk) => {
|
|
50
|
+
write({
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
method: "session/update",
|
|
53
|
+
params: {
|
|
54
|
+
sessionId: sid,
|
|
55
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: chunk } }
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
controller.signal
|
|
60
|
+
);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return replyError(id, -32603, e.message);
|
|
63
|
+
} finally {
|
|
64
|
+
session.controller = null;
|
|
65
|
+
}
|
|
66
|
+
return reply(id, { stopReason: cancelled ? "cancelled" : "end_turn" });
|
|
67
|
+
}
|
|
68
|
+
if (id !== void 0) return replyError(id, -32601, "Method not found");
|
|
69
|
+
};
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
let buf = "";
|
|
72
|
+
const onData = (chunk) => {
|
|
73
|
+
buf += chunk.toString();
|
|
74
|
+
let idx = buf.indexOf("\n");
|
|
75
|
+
while (idx >= 0) {
|
|
76
|
+
const line = buf.slice(0, idx);
|
|
77
|
+
buf = buf.slice(idx + 1);
|
|
78
|
+
idx = buf.indexOf("\n");
|
|
79
|
+
if (!line.trim()) continue;
|
|
80
|
+
let msg;
|
|
81
|
+
try {
|
|
82
|
+
msg = JSON.parse(line);
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
void handle(msg);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const onEnd = () => {
|
|
90
|
+
input.removeListener("data", onData);
|
|
91
|
+
resolve();
|
|
92
|
+
};
|
|
93
|
+
input.on("data", onData);
|
|
94
|
+
input.on("end", onEnd);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function runtimeAgent(cwd) {
|
|
98
|
+
return {
|
|
99
|
+
async run(text, onChunk, signal) {
|
|
100
|
+
const { parseCliArgs } = await import("./args-4SMOHFN5.js");
|
|
101
|
+
const { createRuntime } = await import("./setup-45WQXUBK.js");
|
|
102
|
+
const rt = createRuntime({ cwd, args: { ...parseCliArgs([]), allowAll: true } });
|
|
103
|
+
rt.agent.onEvent = (event) => {
|
|
104
|
+
if (event.type === "text") onChunk(event.delta);
|
|
105
|
+
};
|
|
106
|
+
await rt.agent.run(text, signal);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
ACP_PROTOCOL_VERSION,
|
|
112
|
+
runAcp,
|
|
113
|
+
runtimeAgent
|
|
114
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
helpText,
|
|
4
|
+
parseCliArgs
|
|
5
|
+
} from "./chunk-AS7VV7D3.js";
|
|
6
|
+
import "./chunk-GAFJEQTJ.js";
|
|
7
|
+
import "./chunk-NI5JZUZ3.js";
|
|
8
|
+
import "./chunk-M7LLJQ7M.js";
|
|
9
|
+
import "./chunk-M23QBX3V.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
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MODE_ORDER
|
|
4
|
+
} from "./chunk-GAFJEQTJ.js";
|
|
5
|
+
import {
|
|
6
|
+
REASONING_LEVELS,
|
|
7
|
+
isReasoningLevel
|
|
8
|
+
} from "./chunk-NI5JZUZ3.js";
|
|
9
|
+
import {
|
|
10
|
+
ConfigError
|
|
11
|
+
} from "./chunk-M7LLJQ7M.js";
|
|
12
|
+
import {
|
|
13
|
+
parseLang,
|
|
14
|
+
t
|
|
15
|
+
} from "./chunk-M23QBX3V.js";
|
|
16
|
+
|
|
17
|
+
// src/args.ts
|
|
18
|
+
import { parseArgs } from "util";
|
|
19
|
+
|
|
20
|
+
// src/serveProtocol.ts
|
|
21
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
22
|
+
var MAX_FRAME_BYTES = 262144;
|
|
23
|
+
function parseClientFrame(raw) {
|
|
24
|
+
let value;
|
|
25
|
+
try {
|
|
26
|
+
value = JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
return { error: "invalid JSON frame" };
|
|
29
|
+
}
|
|
30
|
+
const f = value;
|
|
31
|
+
if (typeof f?.streamId !== "string" || f.streamId === "") return { error: "frame requires a streamId" };
|
|
32
|
+
if (f.type === "open") {
|
|
33
|
+
if (typeof f.target !== "string" || f.target === "") return { error: "open requires a target" };
|
|
34
|
+
return { type: "open", streamId: f.streamId, target: f.target };
|
|
35
|
+
}
|
|
36
|
+
if (f.type === "cancel") return { type: "cancel", streamId: f.streamId };
|
|
37
|
+
return { error: `unknown frame type: ${String(f?.type)}` };
|
|
38
|
+
}
|
|
39
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
40
|
+
var LOOPBACK_BIND = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
41
|
+
function trustFence(headers, port) {
|
|
42
|
+
const host = headers.host;
|
|
43
|
+
if (!host) return "missing Host header";
|
|
44
|
+
const sep = host.lastIndexOf(":");
|
|
45
|
+
const hostname = sep > -1 ? host.slice(0, sep) : host;
|
|
46
|
+
const hostPort = sep > -1 ? Number(host.slice(sep + 1)) : 80;
|
|
47
|
+
if (!LOOPBACK_HOSTS.has(hostname)) return `non-loopback Host: ${hostname}`;
|
|
48
|
+
if (hostPort !== port) return `Host port ${hostPort} does not match listener ${port}`;
|
|
49
|
+
if (headers.origin) {
|
|
50
|
+
let origin;
|
|
51
|
+
try {
|
|
52
|
+
origin = new URL(headers.origin);
|
|
53
|
+
} catch {
|
|
54
|
+
return "malformed Origin header";
|
|
55
|
+
}
|
|
56
|
+
const originHost = origin.hostname === "::1" ? "[::1]" : origin.hostname;
|
|
57
|
+
const originPort = origin.port ? Number(origin.port) : origin.protocol === "https:" ? 443 : 80;
|
|
58
|
+
if (!LOOPBACK_HOSTS.has(originHost)) return `cross-origin Origin: ${headers.origin}`;
|
|
59
|
+
if (originPort !== port) return "Origin port mismatch";
|
|
60
|
+
}
|
|
61
|
+
if (headers["sec-fetch-site"] === "cross-site") return "cross-site request";
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function bearerOk(header, token) {
|
|
65
|
+
if (!header?.startsWith("Bearer ")) return false;
|
|
66
|
+
const got = Buffer.from(header.slice(7));
|
|
67
|
+
const want = Buffer.from(token);
|
|
68
|
+
return got.length === want.length && timingSafeEqual(got, want);
|
|
69
|
+
}
|
|
70
|
+
function randomToken() {
|
|
71
|
+
return randomBytes(32).toString("hex");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/args.ts
|
|
75
|
+
var HELP = `BCCLI \u2014 BotConnector's AI coding agent for the terminal
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
bccli [task] interactive mode (optionally start with a task)
|
|
79
|
+
bccli -p "task" run one task without interaction (scripts/CI)
|
|
80
|
+
bccli login [provider] save an API key (default: bc-cloud)
|
|
81
|
+
bccli models [provider] list models (default: the active provider)
|
|
82
|
+
bccli update install the newest bccli from npm
|
|
83
|
+
bccli acp run as an ACP agent for editors (Zed, ...)
|
|
84
|
+
bccli serve [--port N] serve sessions over HTTP+WS for IDEs/bots
|
|
85
|
+
bccli provider list|add <id>|remove <id> manage providers (custom: --url <url> [--name N] [--key-env ENV])
|
|
86
|
+
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
|
|
87
|
+
bccli integrations show the status of external agent integrations
|
|
88
|
+
bccli connect <agent> opencode, aider, cline, dsh, codex, claude-code, cursor, openai-cli, openai-sdk, openai-compatible, openclaw, hermes
|
|
89
|
+
bccli disconnect <agent> remove an integration and restore the previous config
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
-m, --model <provider/model> choose a model, e.g. bc-cloud/glm-5.3-flash
|
|
93
|
+
--reasoning <level> auto | off | low | medium | high | max
|
|
94
|
+
-c, --continue continue the last session in this folder
|
|
95
|
+
-r, --resume pick a session to continue
|
|
96
|
+
--allow-all run all tools without asking for permission
|
|
97
|
+
--allowed-tools <a,b> tools allowed without asking: bash, edit, fetch
|
|
98
|
+
-w, --worktree <name> run in git worktree ../<repo>.worktrees/<name> (created, or reused if it exists)
|
|
99
|
+
--output-format <fmt> text | json | stream-json for -p (machine-readable output)
|
|
100
|
+
--permission-mode <mode> default | acceptEdits | plan | allowAll
|
|
101
|
+
--lang <en|id> interface language (default: en; or BCCLI_LANG, or "language" in config)
|
|
102
|
+
-v, --version version
|
|
103
|
+
-h, --help this help`;
|
|
104
|
+
var helpText = () => t(HELP);
|
|
105
|
+
function parseCliArgs(argv) {
|
|
106
|
+
const { values, positionals } = parseArgs({
|
|
107
|
+
args: argv,
|
|
108
|
+
allowPositionals: true,
|
|
109
|
+
options: {
|
|
110
|
+
print: { type: "boolean", short: "p" },
|
|
111
|
+
model: { type: "string", short: "m" },
|
|
112
|
+
reasoning: { type: "string" },
|
|
113
|
+
lang: { type: "string" },
|
|
114
|
+
continue: { type: "boolean", short: "c" },
|
|
115
|
+
resume: { type: "boolean", short: "r" },
|
|
116
|
+
"allow-all": { type: "boolean" },
|
|
117
|
+
"allowed-tools": { type: "string" },
|
|
118
|
+
worktree: { type: "string", short: "w" },
|
|
119
|
+
"output-format": { type: "string" },
|
|
120
|
+
"permission-mode": { type: "string" },
|
|
121
|
+
help: { type: "boolean", short: "h" },
|
|
122
|
+
version: { type: "boolean", short: "v" },
|
|
123
|
+
url: { type: "string" },
|
|
124
|
+
name: { type: "string" },
|
|
125
|
+
"key-env": { type: "string" },
|
|
126
|
+
value: { type: "string", multiple: true },
|
|
127
|
+
port: { type: "string" },
|
|
128
|
+
host: { type: "string" },
|
|
129
|
+
token: { type: "string" },
|
|
130
|
+
"base-url": { type: "string" }
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const reasoning = values.reasoning;
|
|
134
|
+
if (reasoning && !isReasoningLevel(reasoning)) {
|
|
135
|
+
throw new ConfigError(t("--reasoning must be one of: {levels}", { levels: REASONING_LEVELS.join(", ") }));
|
|
136
|
+
}
|
|
137
|
+
const mode = values["permission-mode"];
|
|
138
|
+
if (mode && !MODE_ORDER.includes(mode)) {
|
|
139
|
+
throw new ConfigError(t("--permission-mode must be one of: {modes}", { modes: MODE_ORDER.join(", ") }));
|
|
140
|
+
}
|
|
141
|
+
if (values.lang !== void 0 && !parseLang(values.lang)) throw new ConfigError(t("--lang must be one of: en, id"));
|
|
142
|
+
const outputFormat = values["output-format"];
|
|
143
|
+
if (outputFormat !== void 0 && !["text", "json", "stream-json"].includes(outputFormat)) {
|
|
144
|
+
throw new ConfigError(t("--output-format must be one of: {formats}", { formats: "text, json, stream-json" }));
|
|
145
|
+
}
|
|
146
|
+
const [first, ...rest] = positionals;
|
|
147
|
+
const SUBCOMMANDS = ["login", "models", "update", "acp", "serve", "provider", "mcp", "connect", "disconnect", "integrations"];
|
|
148
|
+
const command = SUBCOMMANDS.includes(first) ? first : "run";
|
|
149
|
+
const words = command === "run" ? positionals : rest;
|
|
150
|
+
const port = values.port !== void 0 ? Number(values.port) : void 0;
|
|
151
|
+
if (port !== void 0 && (!Number.isInteger(port) || port < 0 || port > 65535)) {
|
|
152
|
+
throw new ConfigError(t("--port must be a number between 0 and 65535"));
|
|
153
|
+
}
|
|
154
|
+
if (values.host !== void 0 && !LOOPBACK_BIND.has(values.host)) {
|
|
155
|
+
throw new ConfigError(t("serve only accepts loopback --host (127.0.0.1, localhost, ::1)"));
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
command,
|
|
159
|
+
prompt: command === "run" && words.length ? words.join(" ") : void 0,
|
|
160
|
+
print: !!values.print,
|
|
161
|
+
model: values.model,
|
|
162
|
+
reasoning,
|
|
163
|
+
lang: parseLang(values.lang),
|
|
164
|
+
continue: !!values.continue,
|
|
165
|
+
resume: !!values.resume,
|
|
166
|
+
allowAll: !!values["allow-all"],
|
|
167
|
+
allowedTools: (values["allowed-tools"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
168
|
+
worktree: values.worktree,
|
|
169
|
+
outputFormat,
|
|
170
|
+
permissionMode: mode,
|
|
171
|
+
help: !!values.help,
|
|
172
|
+
version: !!values.version,
|
|
173
|
+
loginProvider: command === "login" ? rest[0] ?? "bc-cloud" : void 0,
|
|
174
|
+
subArgs: ["models", "provider", "mcp", "connect", "disconnect", "integrations"].includes(command) ? rest : [],
|
|
175
|
+
url: values.url,
|
|
176
|
+
name: values.name,
|
|
177
|
+
keyEnv: values["key-env"],
|
|
178
|
+
values: values.value ?? [],
|
|
179
|
+
port,
|
|
180
|
+
host: values.host,
|
|
181
|
+
token: values.token,
|
|
182
|
+
baseUrl: values["base-url"]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
MAX_FRAME_BYTES,
|
|
188
|
+
parseClientFrame,
|
|
189
|
+
LOOPBACK_BIND,
|
|
190
|
+
trustFence,
|
|
191
|
+
bearerOk,
|
|
192
|
+
randomToken,
|
|
193
|
+
helpText,
|
|
194
|
+
parseCliArgs
|
|
195
|
+
};
|