@botconnector/bccli 0.2.0-beta.1 → 0.3.0-beta.2
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 +59 -14
- package/dist/args-EVIFNGPQ.js +11 -0
- package/dist/chunk-5C6NQFSI.js +50 -0
- package/dist/{chunk-WZYWKWPD.js → chunk-5XN44Q55.js} +5 -77
- package/dist/chunk-E4BWUMQ2.js +52 -0
- package/dist/chunk-MANM7HY6.js +248 -0
- package/dist/chunk-RDQ7HPWS.js +82 -0
- package/dist/chunk-TBPPXOOZ.js +165 -0
- package/dist/chunk-TV6LEHOH.js +45 -0
- package/dist/chunk-XQLXE666.js +69 -0
- package/dist/chunk-Z5JM76EL.js +111 -0
- package/dist/cli.js +458 -367
- package/dist/config-XPWPGNZJ.js +22 -0
- package/dist/login-IXJD5O7Z.js +9 -0
- package/dist/mcpCli-BV3IGR47.js +65 -0
- package/dist/providerCli-7QNAYIFT.js +11 -0
- package/dist/ui-HNIC4DQ6.js +862 -0
- package/package.json +15 -5
- package/dist/ui-U6OMOINK.js +0 -506
package/dist/cli.js
CHANGED
|
@@ -1,346 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
HELP_TEXT,
|
|
4
|
+
parseCliArgs
|
|
5
|
+
} from "./chunk-RDQ7HPWS.js";
|
|
6
|
+
import {
|
|
7
|
+
addProviderKey,
|
|
8
|
+
createProvider,
|
|
9
|
+
listAllModels
|
|
10
|
+
} from "./chunk-MANM7HY6.js";
|
|
11
|
+
import {
|
|
12
|
+
runLogin
|
|
13
|
+
} from "./chunk-E4BWUMQ2.js";
|
|
2
14
|
import {
|
|
3
15
|
Session,
|
|
4
16
|
pruneSessions
|
|
5
17
|
} from "./chunk-SJH3YLW4.js";
|
|
6
18
|
import {
|
|
7
|
-
MODE_ORDER,
|
|
8
|
-
Permissions,
|
|
9
19
|
ignorePatterns,
|
|
20
|
+
loadAgentDefs,
|
|
21
|
+
loadCommands,
|
|
22
|
+
loadSkills,
|
|
23
|
+
parseFrontmatter,
|
|
10
24
|
runPrint
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
permissionMode: "default",
|
|
25
|
-
providers: {
|
|
26
|
-
"bc-cloud": { baseURL: "https://api.botconnector.id/v1", apiKeyEnv: "BOTCONNECTOR_API_KEY" }
|
|
27
|
-
},
|
|
28
|
-
allow: []
|
|
29
|
-
};
|
|
30
|
-
function bccliHome(env = process.env) {
|
|
31
|
-
return env.BCCLI_HOME ?? join(homedir(), ".bccli");
|
|
32
|
-
}
|
|
33
|
-
function readJson(path) {
|
|
34
|
-
if (!existsSync(path)) return {};
|
|
35
|
-
try {
|
|
36
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
37
|
-
} catch (error) {
|
|
38
|
-
throw new ConfigError(`Config tidak valid: ${path} (${error.message})`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function untrustedProviders(project, known) {
|
|
42
|
-
const out = {};
|
|
43
|
-
for (const [id, provider] of Object.entries(project.providers ?? {})) {
|
|
44
|
-
if (known[id] || typeof provider?.baseURL !== "string") continue;
|
|
45
|
-
out[id] = { baseURL: provider.baseURL };
|
|
46
|
-
}
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
|
-
function loadConfig(cwd, env = process.env) {
|
|
50
|
-
const global = readJson(join(bccliHome(env), "config.json"));
|
|
51
|
-
const project = readJson(join(cwd, ".bccli", "config.json"));
|
|
52
|
-
const known = { ...DEFAULT_CONFIG.providers, ...global.providers };
|
|
53
|
-
return {
|
|
54
|
-
model: project.model ?? global.model ?? DEFAULT_CONFIG.model,
|
|
55
|
-
permissionMode: global.permissionMode ?? DEFAULT_CONFIG.permissionMode,
|
|
56
|
-
providers: { ...known, ...untrustedProviders(project, known) },
|
|
57
|
-
allow: [...global.allow ?? []]
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function readCredentials(env = process.env) {
|
|
61
|
-
const path = join(bccliHome(env), "credentials");
|
|
62
|
-
if (!existsSync(path)) return {};
|
|
63
|
-
try {
|
|
64
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
65
|
-
} catch {
|
|
66
|
-
return {};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function saveCredential(providerId, key, env = process.env) {
|
|
70
|
-
const home = bccliHome(env);
|
|
71
|
-
mkdirSync(home, { recursive: true });
|
|
72
|
-
const path = join(home, "credentials");
|
|
73
|
-
writeFileSync(path, JSON.stringify({ ...readCredentials(env), [providerId]: key }, null, 2), { mode: 384 });
|
|
74
|
-
chmodSync(path, 384);
|
|
75
|
-
}
|
|
76
|
-
function resolveModel(config, modelRef, env = process.env) {
|
|
77
|
-
const slash = modelRef.indexOf("/");
|
|
78
|
-
if (slash <= 0 || slash === modelRef.length - 1) {
|
|
79
|
-
throw new ConfigError(`Model harus berformat provider/model, contoh bc-cloud/glm-5.3-flash (dapat: ${modelRef})`);
|
|
80
|
-
}
|
|
81
|
-
const providerId = modelRef.slice(0, slash);
|
|
82
|
-
const provider = config.providers[providerId];
|
|
83
|
-
if (!provider) {
|
|
84
|
-
throw new ConfigError(`Provider "${providerId}" tidak ada di config. Tersedia: ${Object.keys(config.providers).join(", ")}`);
|
|
85
|
-
}
|
|
86
|
-
const apiKey = (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[providerId] || void 0;
|
|
87
|
-
if (provider.apiKeyEnv && !apiKey) {
|
|
88
|
-
throw new ConfigError(`API key untuk ${providerId} belum ada. Jalankan \`bccli login\` atau set env ${provider.apiKeyEnv}.`);
|
|
89
|
-
}
|
|
90
|
-
return { providerId, model: modelRef.slice(slash + 1), baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// src/args.ts
|
|
94
|
-
var HELP_TEXT = `BCCLI \u2014 agent AI BotConnector di terminal
|
|
95
|
-
|
|
96
|
-
Pemakaian:
|
|
97
|
-
bccli [tugas] mode interaktif (opsional langsung dengan tugas)
|
|
98
|
-
bccli -p "tugas" jalankan satu tugas tanpa interaksi (skrip/CI)
|
|
99
|
-
bccli login [provider] simpan API key (default: bc-cloud)
|
|
100
|
-
bccli models daftar model dari provider aktif
|
|
101
|
-
|
|
102
|
-
Opsi:
|
|
103
|
-
-m, --model <provider/model> pilih model, contoh bc-cloud/glm-5.3-flash
|
|
104
|
-
-c, --continue lanjutkan sesi terakhir di folder ini
|
|
105
|
-
-r, --resume pilih sesi untuk dilanjutkan
|
|
106
|
-
--allow-all jalankan semua alat tanpa minta izin
|
|
107
|
-
--allowed-tools <a,b> alat yang boleh tanpa izin: bash, edit, fetch
|
|
108
|
-
--permission-mode <mode> default | acceptEdits | plan | allowAll
|
|
109
|
-
-v, --version versi
|
|
110
|
-
-h, --help bantuan ini`;
|
|
111
|
-
function parseCliArgs(argv) {
|
|
112
|
-
const { values, positionals } = parseArgs({
|
|
113
|
-
args: argv,
|
|
114
|
-
allowPositionals: true,
|
|
115
|
-
options: {
|
|
116
|
-
print: { type: "boolean", short: "p" },
|
|
117
|
-
model: { type: "string", short: "m" },
|
|
118
|
-
continue: { type: "boolean", short: "c" },
|
|
119
|
-
resume: { type: "boolean", short: "r" },
|
|
120
|
-
"allow-all": { type: "boolean" },
|
|
121
|
-
"allowed-tools": { type: "string" },
|
|
122
|
-
"permission-mode": { type: "string" },
|
|
123
|
-
help: { type: "boolean", short: "h" },
|
|
124
|
-
version: { type: "boolean", short: "v" }
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
const mode = values["permission-mode"];
|
|
128
|
-
if (mode && !MODE_ORDER.includes(mode)) {
|
|
129
|
-
throw new ConfigError(`--permission-mode harus salah satu dari: ${MODE_ORDER.join(", ")}`);
|
|
130
|
-
}
|
|
131
|
-
const [first, ...rest] = positionals;
|
|
132
|
-
const command = first === "login" || first === "models" ? first : "run";
|
|
133
|
-
const words = command === "run" ? positionals : rest;
|
|
134
|
-
return {
|
|
135
|
-
command,
|
|
136
|
-
prompt: command === "run" && words.length ? words.join(" ") : void 0,
|
|
137
|
-
print: !!values.print,
|
|
138
|
-
model: values.model,
|
|
139
|
-
continue: !!values.continue,
|
|
140
|
-
resume: !!values.resume,
|
|
141
|
-
allowAll: !!values["allow-all"],
|
|
142
|
-
allowedTools: (values["allowed-tools"] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
143
|
-
permissionMode: mode,
|
|
144
|
-
help: !!values.help,
|
|
145
|
-
version: !!values.version,
|
|
146
|
-
loginProvider: command === "login" ? rest[0] ?? "bc-cloud" : void 0
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// src/provider.ts
|
|
151
|
-
var ProviderError = class extends Error {
|
|
152
|
-
constructor(message, status) {
|
|
153
|
-
super(message);
|
|
154
|
-
this.status = status;
|
|
155
|
-
}
|
|
156
|
-
status;
|
|
157
|
-
};
|
|
158
|
-
var retryable = (status) => status === 429 || status >= 500;
|
|
159
|
-
function sleep(ms, signal) {
|
|
160
|
-
return new Promise((resolve2, reject) => {
|
|
161
|
-
const timer = setTimeout(resolve2, ms);
|
|
162
|
-
signal?.addEventListener(
|
|
163
|
-
"abort",
|
|
164
|
-
() => {
|
|
165
|
-
clearTimeout(timer);
|
|
166
|
-
reject(signal.reason);
|
|
167
|
-
},
|
|
168
|
-
{ once: true }
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
function errorMessage(body) {
|
|
173
|
-
try {
|
|
174
|
-
const parsed = JSON.parse(body);
|
|
175
|
-
return parsed?.error?.message ?? parsed?.message ?? body.slice(0, 300);
|
|
176
|
-
} catch {
|
|
177
|
-
return body.slice(0, 300);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
var toUsage = (u) => u ? { inputTokens: u.prompt_tokens ?? 0, outputTokens: u.completion_tokens ?? 0 } : void 0;
|
|
181
|
-
async function readStream(res, onText) {
|
|
182
|
-
let text = "";
|
|
183
|
-
let usage;
|
|
184
|
-
let finishReason;
|
|
185
|
-
const calls = [];
|
|
186
|
-
const byIndex = /* @__PURE__ */ new Map();
|
|
187
|
-
let current;
|
|
188
|
-
const decoder = new TextDecoder();
|
|
189
|
-
let buffer = "";
|
|
190
|
-
const handle = (line) => {
|
|
191
|
-
if (!line.startsWith("data:")) return;
|
|
192
|
-
const data = line.slice(5).trim();
|
|
193
|
-
if (!data || data === "[DONE]") return;
|
|
194
|
-
let event;
|
|
195
|
-
try {
|
|
196
|
-
event = JSON.parse(data);
|
|
197
|
-
} catch {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (event.error) throw new ProviderError(`Error dari provider: ${event.error.message ?? JSON.stringify(event.error)}`);
|
|
201
|
-
if (event.usage) usage = toUsage(event.usage);
|
|
202
|
-
const choice = event.choices?.[0];
|
|
203
|
-
if (!choice) return;
|
|
204
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
205
|
-
const delta = choice.delta ?? {};
|
|
206
|
-
if (delta.content) {
|
|
207
|
-
text += delta.content;
|
|
208
|
-
onText?.(delta.content);
|
|
209
|
-
}
|
|
210
|
-
for (const tc of delta.tool_calls ?? []) {
|
|
211
|
-
let target = tc.index !== void 0 ? byIndex.get(tc.index) : current;
|
|
212
|
-
if (!target || tc.index === void 0 && tc.id && target.id && target.id !== tc.id) {
|
|
213
|
-
target = { id: "", name: "", arguments: "" };
|
|
214
|
-
calls.push(target);
|
|
215
|
-
if (tc.index !== void 0) byIndex.set(tc.index, target);
|
|
216
|
-
}
|
|
217
|
-
current = target;
|
|
218
|
-
if (tc.id) target.id = tc.id;
|
|
219
|
-
if (tc.function?.name && !target.name) target.name = tc.function.name;
|
|
220
|
-
if (tc.function?.arguments) target.arguments += tc.function.arguments;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
for await (const chunk of res.body) {
|
|
224
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
225
|
-
let newline = buffer.indexOf("\n");
|
|
226
|
-
while (newline >= 0) {
|
|
227
|
-
handle(buffer.slice(0, newline).trim());
|
|
228
|
-
buffer = buffer.slice(newline + 1);
|
|
229
|
-
newline = buffer.indexOf("\n");
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
handle(buffer.trim());
|
|
233
|
-
const toolCalls = calls.map((call, index) => ({ ...call, id: call.id || `call_${index}` }));
|
|
234
|
-
return { text, toolCalls, usage, finishReason };
|
|
235
|
-
}
|
|
236
|
-
async function readJson2(res) {
|
|
237
|
-
const body = await res.json();
|
|
238
|
-
const choice = body.choices?.[0];
|
|
239
|
-
const toolCalls = (choice?.message?.tool_calls ?? []).map((tc, i) => ({
|
|
240
|
-
id: tc.id || `call_${i}`,
|
|
241
|
-
name: tc.function?.name ?? "",
|
|
242
|
-
arguments: tc.function?.arguments ?? ""
|
|
243
|
-
}));
|
|
244
|
-
return { text: choice?.message?.content ?? "", toolCalls, usage: toUsage(body.usage), finishReason: choice?.finish_reason };
|
|
245
|
-
}
|
|
246
|
-
function createProvider({ baseURL, apiKey, model, fetch: doFetch = fetch, retryDelayMs = 1e3 }) {
|
|
247
|
-
const headers = { "content-type": "application/json" };
|
|
248
|
-
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
|
|
249
|
-
async function post(path, body, signal) {
|
|
250
|
-
let lastError;
|
|
251
|
-
for (let attempt = 0; attempt < 4; attempt++) {
|
|
252
|
-
if (attempt > 0) await sleep(retryDelayMs * 2 ** (attempt - 1), signal);
|
|
253
|
-
let res;
|
|
254
|
-
try {
|
|
255
|
-
res = await doFetch(`${baseURL}${path}`, { method: "POST", headers, body: JSON.stringify(body), signal });
|
|
256
|
-
} catch (error) {
|
|
257
|
-
if (signal?.aborted) throw error;
|
|
258
|
-
lastError = new ProviderError(`Tidak bisa terhubung ke ${baseURL}: ${error.message}`);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (res.ok) return res;
|
|
262
|
-
const text = await res.text().catch(() => "");
|
|
263
|
-
lastError = new ProviderError(`${res.status} dari ${baseURL}: ${errorMessage(text)}`, res.status);
|
|
264
|
-
if (!retryable(res.status)) throw lastError;
|
|
265
|
-
}
|
|
266
|
-
throw lastError ?? new ProviderError("Permintaan gagal");
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
async chat({ messages, tools, signal, onText }) {
|
|
270
|
-
const res = await post(
|
|
271
|
-
"/chat/completions",
|
|
272
|
-
{ model, messages, stream: true, stream_options: { include_usage: true }, ...tools?.length ? { tools } : {} },
|
|
273
|
-
signal
|
|
274
|
-
);
|
|
275
|
-
if ((res.headers.get("content-type") ?? "").includes("application/json")) return readJson2(res);
|
|
276
|
-
if (!res.body) throw new ProviderError("Respons provider kosong");
|
|
277
|
-
return readStream(res, onText);
|
|
278
|
-
},
|
|
279
|
-
async listModels() {
|
|
280
|
-
const res = await doFetch(`${baseURL}/models`, { headers });
|
|
281
|
-
if (!res.ok) throw new ProviderError(`${res.status} saat mengambil daftar model`, res.status);
|
|
282
|
-
const body = await res.json();
|
|
283
|
-
return (body.data ?? []).map((m) => m.id).sort();
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// src/login.ts
|
|
289
|
-
function readSecret(prompt) {
|
|
290
|
-
return new Promise((resolve2) => {
|
|
291
|
-
process.stdout.write(prompt);
|
|
292
|
-
const stdin = process.stdin;
|
|
293
|
-
if (!stdin.isTTY) {
|
|
294
|
-
let data = "";
|
|
295
|
-
stdin.on("data", (d) => {
|
|
296
|
-
data += d;
|
|
297
|
-
});
|
|
298
|
-
stdin.on("end", () => resolve2(data.trim()));
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
let value = "";
|
|
302
|
-
stdin.setRawMode(true);
|
|
303
|
-
stdin.resume();
|
|
304
|
-
const onData = (buffer) => {
|
|
305
|
-
for (const ch of buffer.toString()) {
|
|
306
|
-
if (ch === "\r" || ch === "\n") {
|
|
307
|
-
stdin.setRawMode(false);
|
|
308
|
-
stdin.pause();
|
|
309
|
-
stdin.off("data", onData);
|
|
310
|
-
process.stdout.write("\n");
|
|
311
|
-
resolve2(value.trim());
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
if (ch === "") process.exit(130);
|
|
315
|
-
if (ch === "\x7F" || ch === "\b") value = value.slice(0, -1);
|
|
316
|
-
else value += ch;
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
stdin.on("data", onData);
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
async function runLogin(providerId, cwd) {
|
|
323
|
-
const config = loadConfig(cwd);
|
|
324
|
-
const provider = config.providers[providerId];
|
|
325
|
-
if (!provider) {
|
|
326
|
-
console.error(`Provider "${providerId}" tidak ada. Tersedia: ${Object.keys(config.providers).join(", ")}`);
|
|
327
|
-
return 1;
|
|
328
|
-
}
|
|
329
|
-
const key = await readSecret(`API key untuk ${providerId} (${provider.baseURL}): `);
|
|
330
|
-
if (!key) {
|
|
331
|
-
console.error("API key kosong, tidak disimpan.");
|
|
332
|
-
return 1;
|
|
333
|
-
}
|
|
334
|
-
try {
|
|
335
|
-
const models = await createProvider({ baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey: key, model: "" }).listModels();
|
|
336
|
-
saveCredential(providerId, key);
|
|
337
|
-
console.log(`Tersimpan. ${models.length} model tersedia di ${providerId}.`);
|
|
338
|
-
return 0;
|
|
339
|
-
} catch (error) {
|
|
340
|
-
console.error(`API key ditolak atau provider tidak bisa dihubungi: ${error.message}`);
|
|
341
|
-
return 1;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
25
|
+
} from "./chunk-TBPPXOOZ.js";
|
|
26
|
+
import {
|
|
27
|
+
Permissions
|
|
28
|
+
} from "./chunk-5XN44Q55.js";
|
|
29
|
+
import {
|
|
30
|
+
providerName
|
|
31
|
+
} from "./chunk-5C6NQFSI.js";
|
|
32
|
+
import {
|
|
33
|
+
ConfigError,
|
|
34
|
+
bccliHome,
|
|
35
|
+
loadConfig,
|
|
36
|
+
resolveModel
|
|
37
|
+
} from "./chunk-Z5JM76EL.js";
|
|
344
38
|
|
|
345
39
|
// src/tools/index.ts
|
|
346
40
|
import { z as z8 } from "zod";
|
|
@@ -775,7 +469,7 @@ var readTool = defineTool({
|
|
|
775
469
|
});
|
|
776
470
|
|
|
777
471
|
// src/tools/write.ts
|
|
778
|
-
import { existsSync
|
|
472
|
+
import { existsSync } from "fs";
|
|
779
473
|
import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
780
474
|
import { dirname } from "path";
|
|
781
475
|
import { z as z7 } from "zod";
|
|
@@ -790,16 +484,16 @@ var writeTool = defineTool({
|
|
|
790
484
|
target: (input) => input.path,
|
|
791
485
|
async validate(input, ctx) {
|
|
792
486
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
793
|
-
return
|
|
487
|
+
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.` : void 0;
|
|
794
488
|
},
|
|
795
489
|
async preview(input, ctx) {
|
|
796
490
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
797
|
-
const old =
|
|
491
|
+
const old = existsSync(abs) ? await readFile4(abs, "utf8") : "";
|
|
798
492
|
return formatDiff(diffLines(old, input.content));
|
|
799
493
|
},
|
|
800
494
|
async run(input, ctx) {
|
|
801
495
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
802
|
-
if (
|
|
496
|
+
if (existsSync(abs) && !ctx.readFiles.has(abs)) {
|
|
803
497
|
return { output: `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
|
|
804
498
|
}
|
|
805
499
|
try {
|
|
@@ -818,7 +512,7 @@ var writeTool = defineTool({
|
|
|
818
512
|
var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fetchTool];
|
|
819
513
|
function toolDefinitions(tools) {
|
|
820
514
|
return tools.map((tool) => {
|
|
821
|
-
const { $schema: _ignored, ...parameters } = z8.toJSONSchema(tool.schema);
|
|
515
|
+
const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z8.toJSONSchema(tool.schema);
|
|
822
516
|
return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
|
|
823
517
|
});
|
|
824
518
|
}
|
|
@@ -833,6 +527,8 @@ var Agent = class {
|
|
|
833
527
|
this.permissions = opts.permissions;
|
|
834
528
|
this.tools = opts.tools;
|
|
835
529
|
this.definitions = toolDefinitions(opts.tools);
|
|
530
|
+
this.turnTools = this.tools;
|
|
531
|
+
this.turnDefinitions = this.definitions;
|
|
836
532
|
this.messages = [...opts.history ?? []];
|
|
837
533
|
this.maxSteps = opts.maxSteps ?? 50;
|
|
838
534
|
this.contextWindow = opts.contextWindow ?? 128e3;
|
|
@@ -848,10 +544,27 @@ var Agent = class {
|
|
|
848
544
|
askPermission = async () => "no";
|
|
849
545
|
tools;
|
|
850
546
|
definitions;
|
|
547
|
+
// Snapshot for the running turn: tools added/removed mid-turn (e.g. an MCP server connecting) apply from the next turn.
|
|
548
|
+
turnTools;
|
|
549
|
+
turnDefinitions;
|
|
851
550
|
readFiles = /* @__PURE__ */ new Set();
|
|
852
551
|
maxSteps;
|
|
853
552
|
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
854
553
|
contextWindow;
|
|
554
|
+
setTools(tools) {
|
|
555
|
+
this.tools = tools;
|
|
556
|
+
this.definitions = toolDefinitions(tools);
|
|
557
|
+
}
|
|
558
|
+
isParallelSafe(call) {
|
|
559
|
+
const tool = this.turnTools.find((t) => t.name === call.name);
|
|
560
|
+
if (!tool?.parallelSafe) return false;
|
|
561
|
+
try {
|
|
562
|
+
const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
|
|
563
|
+
return parsed.success && tool.parallelSafe(parsed.data);
|
|
564
|
+
} catch {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
855
568
|
push(message) {
|
|
856
569
|
this.messages.push(message);
|
|
857
570
|
this.opts.onMessage?.(message);
|
|
@@ -876,6 +589,8 @@ ${completion.text}` });
|
|
|
876
589
|
this.onEvent({ type: "compacted" });
|
|
877
590
|
}
|
|
878
591
|
async run(text, signal) {
|
|
592
|
+
this.turnTools = this.tools;
|
|
593
|
+
this.turnDefinitions = this.definitions;
|
|
879
594
|
try {
|
|
880
595
|
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
881
596
|
this.push({ role: "user", content: text });
|
|
@@ -886,7 +601,7 @@ ${completion.text}` });
|
|
|
886
601
|
}
|
|
887
602
|
const completion = await this.provider.chat({
|
|
888
603
|
messages: [{ role: "system", content: this.opts.systemPrompt }, ...this.messages],
|
|
889
|
-
tools: this.
|
|
604
|
+
tools: this.turnDefinitions,
|
|
890
605
|
signal,
|
|
891
606
|
onText: (delta) => this.onEvent({ type: "text", delta })
|
|
892
607
|
});
|
|
@@ -913,16 +628,22 @@ ${completion.text}` });
|
|
|
913
628
|
this.onEvent({ type: "done" });
|
|
914
629
|
return;
|
|
915
630
|
}
|
|
916
|
-
|
|
917
|
-
|
|
631
|
+
const calls = completion.toolCalls;
|
|
632
|
+
let i = 0;
|
|
633
|
+
while (i < calls.length) {
|
|
918
634
|
if (signal.aborted) {
|
|
919
|
-
for (const pending of
|
|
920
|
-
this.push({ role: "tool", tool_call_id: pending.id, content: "Dibatalkan oleh user." });
|
|
921
|
-
}
|
|
635
|
+
for (const pending of calls.slice(i)) this.push({ role: "tool", tool_call_id: pending.id, content: "Dibatalkan oleh user." });
|
|
922
636
|
this.onEvent({ type: "aborted" });
|
|
923
637
|
return;
|
|
924
638
|
}
|
|
925
|
-
|
|
639
|
+
let j = i + 1;
|
|
640
|
+
if (this.isParallelSafe(calls[i])) while (j < calls.length && this.isParallelSafe(calls[j])) j++;
|
|
641
|
+
const batch = calls.slice(i, j);
|
|
642
|
+
const results = await Promise.all(batch.map((c) => this.runTool(c, signal)));
|
|
643
|
+
batch.forEach((c, k) => {
|
|
644
|
+
this.push({ role: "tool", tool_call_id: c.id, content: results[k] });
|
|
645
|
+
});
|
|
646
|
+
i = j;
|
|
926
647
|
}
|
|
927
648
|
if (signal.aborted) {
|
|
928
649
|
this.onEvent({ type: "aborted" });
|
|
@@ -950,8 +671,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
950
671
|
this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
|
|
951
672
|
return output;
|
|
952
673
|
};
|
|
953
|
-
const tool = this.
|
|
954
|
-
if (!tool) return fail(`Alat "${call.name}" tidak ada. Alat yang tersedia: ${this.
|
|
674
|
+
const tool = this.turnTools.find((t) => t.name === call.name);
|
|
675
|
+
if (!tool) return fail(`Alat "${call.name}" tidak ada. Alat yang tersedia: ${this.turnTools.map((t) => t.name).join(", ")}`);
|
|
955
676
|
let args;
|
|
956
677
|
try {
|
|
957
678
|
args = JSON.parse(call.arguments || "{}");
|
|
@@ -964,7 +685,19 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
964
685
|
}
|
|
965
686
|
const input = parsed.data;
|
|
966
687
|
const target = tool.target(input);
|
|
967
|
-
const ctx = {
|
|
688
|
+
const ctx = {
|
|
689
|
+
cwd: this.opts.cwd,
|
|
690
|
+
signal,
|
|
691
|
+
readFiles: this.readFiles,
|
|
692
|
+
callId: call.id,
|
|
693
|
+
emit: (event) => this.onEvent(event),
|
|
694
|
+
ask: (req) => this.askPermission(req),
|
|
695
|
+
addUsage: (u) => {
|
|
696
|
+
this.totalUsage.inputTokens += u.inputTokens;
|
|
697
|
+
this.totalUsage.outputTokens += u.outputTokens;
|
|
698
|
+
this.onEvent({ type: "usage", ...this.totalUsage });
|
|
699
|
+
}
|
|
700
|
+
};
|
|
968
701
|
this.onEvent({ type: "toolStart", id: call.id, tool: tool.name, target });
|
|
969
702
|
const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
|
|
970
703
|
if (invalid) {
|
|
@@ -975,8 +708,14 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
975
708
|
let decision = this.permissions.check(request);
|
|
976
709
|
if (decision === "ask") {
|
|
977
710
|
const preview = await tool.preview?.(input, ctx).catch(() => void 0);
|
|
978
|
-
const answer = await this.askPermission({
|
|
711
|
+
const answer = await this.askPermission({
|
|
712
|
+
...request,
|
|
713
|
+
preview,
|
|
714
|
+
sessionRules: this.permissions.rulesFor(request),
|
|
715
|
+
...this.opts.label ? { agent: this.opts.label } : {}
|
|
716
|
+
});
|
|
979
717
|
if (answer === "session") this.permissions.allowForSession(request);
|
|
718
|
+
if (answer === "all") this.permissions.mode = "allowAll";
|
|
980
719
|
decision = answer === "no" ? "deny" : "allow";
|
|
981
720
|
}
|
|
982
721
|
if (decision === "deny") {
|
|
@@ -995,13 +734,142 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
995
734
|
}
|
|
996
735
|
};
|
|
997
736
|
|
|
737
|
+
// src/agents.ts
|
|
738
|
+
var EXPLORE_PROMPT = "You are a read-only research agent working for another agent. Search and read the codebase to answer the request. You cannot edit files or run commands. Reply with a concise report: the answer, the relevant file paths with line numbers, and nothing else.";
|
|
739
|
+
var GENERAL_PROMPT = "You are an agent handling a delegated task for another agent. Complete the task end to end with the tools you have, verify the result, and reply with a short report of what you did and what you found.";
|
|
740
|
+
var BUILTIN_AGENTS = [
|
|
741
|
+
{
|
|
742
|
+
name: "explore",
|
|
743
|
+
description: "Read-only: search and read code to answer a question (fast, safe, can run in parallel).",
|
|
744
|
+
tools: ["read", "grep", "glob"],
|
|
745
|
+
prompt: EXPLORE_PROMPT,
|
|
746
|
+
maxSteps: 30
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: "general",
|
|
750
|
+
description: "Full tools: carry out a self-contained sub-task (edits and commands still need user permission).",
|
|
751
|
+
prompt: GENERAL_PROMPT,
|
|
752
|
+
maxSteps: 50
|
|
753
|
+
}
|
|
754
|
+
];
|
|
755
|
+
|
|
756
|
+
// src/mcp/manager.ts
|
|
757
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
758
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
759
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
760
|
+
import { z as z9 } from "zod";
|
|
761
|
+
function mcpToolName(server, tool, taken) {
|
|
762
|
+
const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
763
|
+
const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
|
|
764
|
+
let name = base;
|
|
765
|
+
for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
|
|
766
|
+
taken.add(name);
|
|
767
|
+
return name;
|
|
768
|
+
}
|
|
769
|
+
function withTimeout(promise, ms) {
|
|
770
|
+
let timer;
|
|
771
|
+
const timeout = new Promise((_, reject) => {
|
|
772
|
+
timer = setTimeout(() => reject(new Error(`timeout ${Math.round(ms / 1e3)} detik`)), ms);
|
|
773
|
+
});
|
|
774
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
775
|
+
}
|
|
776
|
+
var McpManager = class {
|
|
777
|
+
constructor(opts = {}) {
|
|
778
|
+
this.opts = opts;
|
|
779
|
+
}
|
|
780
|
+
opts;
|
|
781
|
+
servers = /* @__PURE__ */ new Map();
|
|
782
|
+
taken = /* @__PURE__ */ new Set();
|
|
783
|
+
async start(specs) {
|
|
784
|
+
await Promise.all(specs.map((spec) => this.add(spec)));
|
|
785
|
+
}
|
|
786
|
+
async add(spec) {
|
|
787
|
+
await this.remove(spec.name);
|
|
788
|
+
const entry = { spec, state: { name: spec.name, source: spec.source, status: "starting", tools: 0 }, tools: [] };
|
|
789
|
+
this.servers.set(spec.name, entry);
|
|
790
|
+
this.opts.onChange?.();
|
|
791
|
+
const client = new Client({ name: "bccli", version: "0.3.0" });
|
|
792
|
+
entry.client = client;
|
|
793
|
+
const timeoutMs = this.opts.connectTimeoutMs ?? 3e4;
|
|
794
|
+
try {
|
|
795
|
+
const config = spec.config;
|
|
796
|
+
const transport = config.type === "http" ? new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers: config.headers ?? {} } }) : new StdioClientTransport({
|
|
797
|
+
command: config.command,
|
|
798
|
+
args: config.args ?? [],
|
|
799
|
+
env: { ...process.env, ...config.env },
|
|
800
|
+
stderr: "ignore"
|
|
801
|
+
});
|
|
802
|
+
await withTimeout(client.connect(transport), timeoutMs);
|
|
803
|
+
const { tools } = await withTimeout(client.listTools(), timeoutMs);
|
|
804
|
+
if (this.servers.get(spec.name) !== entry) {
|
|
805
|
+
await client.close().catch(() => {
|
|
806
|
+
});
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
entry.tools = tools.map((t) => this.wrap(spec.name, client, t));
|
|
810
|
+
entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
|
|
811
|
+
} catch (error) {
|
|
812
|
+
entry.state = { ...entry.state, status: "error", error: error.message };
|
|
813
|
+
await client.close().catch(() => {
|
|
814
|
+
});
|
|
815
|
+
if (this.servers.get(spec.name) !== entry) return;
|
|
816
|
+
}
|
|
817
|
+
this.opts.onChange?.();
|
|
818
|
+
}
|
|
819
|
+
wrap(server, client, tool) {
|
|
820
|
+
return {
|
|
821
|
+
name: mcpToolName(server, tool.name, this.taken),
|
|
822
|
+
description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
|
|
823
|
+
schema: z9.record(z9.string(), z9.unknown()),
|
|
824
|
+
jsonSchema: tool.inputSchema,
|
|
825
|
+
kind: "mcp",
|
|
826
|
+
target: (input) => {
|
|
827
|
+
const text = JSON.stringify(input);
|
|
828
|
+
return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
829
|
+
},
|
|
830
|
+
async run(input, ctx) {
|
|
831
|
+
try {
|
|
832
|
+
const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
|
|
833
|
+
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(kosong)";
|
|
834
|
+
return { output: output.length > 5e4 ? `${output.slice(0, 5e4)}
|
|
835
|
+
\u2026 [dipotong]` : output, isError: !!result.isError };
|
|
836
|
+
} catch (error) {
|
|
837
|
+
return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async remove(name) {
|
|
843
|
+
const entry = this.servers.get(name);
|
|
844
|
+
if (!entry) return;
|
|
845
|
+
this.servers.delete(name);
|
|
846
|
+
for (const t of entry.tools) this.taken.delete(t.name);
|
|
847
|
+
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t) => t.name));
|
|
848
|
+
await entry.client?.close().catch(() => {
|
|
849
|
+
});
|
|
850
|
+
this.opts.onChange?.();
|
|
851
|
+
}
|
|
852
|
+
tools() {
|
|
853
|
+
return [...this.servers.values()].flatMap((s) => s.tools);
|
|
854
|
+
}
|
|
855
|
+
states() {
|
|
856
|
+
return [...this.servers.values()].map((s) => s.state);
|
|
857
|
+
}
|
|
858
|
+
async stop() {
|
|
859
|
+
await Promise.all([...this.servers.keys()].map((name) => this.remove(name)));
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// src/setup.ts
|
|
864
|
+
import { homedir } from "os";
|
|
865
|
+
|
|
998
866
|
// src/context.ts
|
|
999
|
-
import { existsSync as
|
|
1000
|
-
import { dirname as dirname2, join
|
|
867
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
868
|
+
import { dirname as dirname2, join } from "path";
|
|
1001
869
|
var FILES = ["AGENTS.md", "BCCLI.md"];
|
|
1002
870
|
function readIf(path) {
|
|
1003
871
|
try {
|
|
1004
|
-
return
|
|
872
|
+
return existsSync2(path) ? readFileSync(path, "utf8").trim() : void 0;
|
|
1005
873
|
} catch {
|
|
1006
874
|
return void 0;
|
|
1007
875
|
}
|
|
@@ -1013,13 +881,13 @@ function loadInstructions(cwd, home) {
|
|
|
1013
881
|
if (dirname2(dir) === dir) break;
|
|
1014
882
|
}
|
|
1015
883
|
const parts = [];
|
|
1016
|
-
const global = readIf(
|
|
1017
|
-
if (global) parts.push(`# ${
|
|
884
|
+
const global = readIf(join(home, "BCCLI.md"));
|
|
885
|
+
if (global) parts.push(`# ${join(home, "BCCLI.md")}
|
|
1018
886
|
${global}`);
|
|
1019
887
|
for (const dir of dirs) {
|
|
1020
888
|
for (const name of FILES) {
|
|
1021
|
-
const text = readIf(
|
|
1022
|
-
if (text) parts.push(`# ${
|
|
889
|
+
const text = readIf(join(dir, name));
|
|
890
|
+
if (text) parts.push(`# ${join(dir, name)}
|
|
1023
891
|
${text}`);
|
|
1024
892
|
}
|
|
1025
893
|
}
|
|
@@ -1027,17 +895,24 @@ ${text}`);
|
|
|
1027
895
|
}
|
|
1028
896
|
function isGitRepo(cwd) {
|
|
1029
897
|
for (let dir = cwd; ; dir = dirname2(dir)) {
|
|
1030
|
-
if (
|
|
898
|
+
if (existsSync2(join(dir, ".git"))) return true;
|
|
1031
899
|
if (dirname2(dir) === dir) return false;
|
|
1032
900
|
}
|
|
1033
901
|
}
|
|
1034
902
|
function buildSystemPrompt(opts) {
|
|
1035
903
|
const instructions = loadInstructions(opts.cwd, opts.home);
|
|
904
|
+
const skills = (opts.skills ?? []).slice(0, 50);
|
|
905
|
+
const skillText = skills.length ? `
|
|
906
|
+
|
|
907
|
+
Skills (load one with the skill tool when it matches the task):
|
|
908
|
+
${skills.map((s) => `- ${s.name}: ${s.description.slice(0, 200)}`).join("\n")}` : "";
|
|
1036
909
|
return `You are BCCLI, a coding agent by BotConnector running in the user's terminal. You help with software engineering tasks by reading code, editing files, and running commands with the tools provided.
|
|
1037
910
|
|
|
1038
911
|
How to work:
|
|
1039
912
|
- Understand before changing: read the relevant files and search the codebase first. Never guess file contents.
|
|
1040
913
|
- Always read a file before editing it. Make the smallest correct change; match the existing style.
|
|
914
|
+
- For tasks with 3 or more steps, keep a todo list with todo_write and update it as you go.
|
|
915
|
+
- In plan mode you can only read and search. When the plan is ready, call exit_plan with it and wait for approval.
|
|
1041
916
|
- Verify your work: run the tests, build or the command that proves the change works, and report the real result.
|
|
1042
917
|
- The user must approve edits and commands. If they decline, ask what they want instead of retrying.
|
|
1043
918
|
- Be concise. Reply in the user's language. No preamble; lead with the answer or the action.
|
|
@@ -1048,19 +923,168 @@ Environment:
|
|
|
1048
923
|
- Git repository: ${isGitRepo(opts.cwd) ? "yes" : "no"}
|
|
1049
924
|
- Platform: ${opts.platform ?? process.platform}
|
|
1050
925
|
- Date: ${opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
1051
|
-
- Model: ${opts.model}${instructions ? `
|
|
926
|
+
- Model: ${opts.model}${skillText}${instructions ? `
|
|
1052
927
|
|
|
1053
928
|
Project and user instructions (follow them):
|
|
1054
929
|
|
|
1055
930
|
${instructions}` : ""}`;
|
|
1056
931
|
}
|
|
1057
932
|
|
|
933
|
+
// src/tools/plan.ts
|
|
934
|
+
import { z as z10 } from "zod";
|
|
935
|
+
function createExitPlanTool(opts) {
|
|
936
|
+
return defineTool({
|
|
937
|
+
name: "exit_plan",
|
|
938
|
+
description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
|
|
939
|
+
schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
|
|
940
|
+
kind: "read",
|
|
941
|
+
target: () => "rencana",
|
|
942
|
+
async run(input) {
|
|
943
|
+
if (opts.permissions.mode !== "plan") return { output: "exit_plan hanya dipakai di mode plan.", isError: true };
|
|
944
|
+
const decision = await opts.interaction.approvePlan(input.plan);
|
|
945
|
+
if (decision === "no") {
|
|
946
|
+
return { output: "User belum menyetujui. Tanyakan apa yang perlu diperbaiki, perbaiki rencananya, lalu panggil exit_plan lagi." };
|
|
947
|
+
}
|
|
948
|
+
opts.permissions.mode = decision;
|
|
949
|
+
return { output: `Rencana disetujui. Mode sekarang ${decision}. Mulai kerjakan rencananya sekarang.` };
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/tools/skill.ts
|
|
955
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
956
|
+
import { z as z11 } from "zod";
|
|
957
|
+
function createSkillTool(skills) {
|
|
958
|
+
return defineTool({
|
|
959
|
+
name: "skill",
|
|
960
|
+
description: "Load a skill (instructions for a kind of task) by name. Use it when a listed skill matches the task, then follow it.",
|
|
961
|
+
schema: z11.object({ name: z11.string().describe("Skill name from the list in the system prompt") }),
|
|
962
|
+
kind: "read",
|
|
963
|
+
target: (input) => input.name,
|
|
964
|
+
async run(input) {
|
|
965
|
+
const skill = skills.find((s) => s.name === input.name);
|
|
966
|
+
if (!skill) return { output: `Skill "${input.name}" tidak ada. Tersedia: ${skills.map((s) => s.name).join(", ") || "(tidak ada)"}`, isError: true };
|
|
967
|
+
const { body } = parseFrontmatter(readFileSync2(skill.file, "utf8"));
|
|
968
|
+
return { output: `Base directory for this skill: ${skill.dir}
|
|
969
|
+
|
|
970
|
+
${body}`, display: skill.description.slice(0, 60) };
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/tools/task.ts
|
|
976
|
+
import { z as z12 } from "zod";
|
|
977
|
+
function createTaskTool(opts) {
|
|
978
|
+
const byName = new Map(opts.agents.map((a) => [a.name, a]));
|
|
979
|
+
const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
980
|
+
return defineTool({
|
|
981
|
+
name: "task",
|
|
982
|
+
description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
|
|
983
|
+
${list}`,
|
|
984
|
+
schema: z12.object({
|
|
985
|
+
agent: z12.string().describe("Agent name"),
|
|
986
|
+
description: z12.string().describe("3-6 word summary shown to the user"),
|
|
987
|
+
prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
|
|
988
|
+
}),
|
|
989
|
+
kind: "read",
|
|
990
|
+
target: (input) => `[${input.agent}] ${input.description}`,
|
|
991
|
+
// Parallel only when the resolved agent can do nothing but read (a project file may redefine "explore").
|
|
992
|
+
parallelSafe: (input) => {
|
|
993
|
+
const allowed = byName.get(input.agent)?.tools;
|
|
994
|
+
if (!allowed) return false;
|
|
995
|
+
const base = opts.baseTools();
|
|
996
|
+
return allowed.every((name) => base.find((t) => t.name === name)?.kind === "read");
|
|
997
|
+
},
|
|
998
|
+
async run(input, ctx) {
|
|
999
|
+
const def = byName.get(input.agent);
|
|
1000
|
+
if (!def) return { output: `Agent "${input.agent}" tidak ada. Tersedia: ${[...byName.keys()].join(", ")}`, isError: true };
|
|
1001
|
+
const base = opts.baseTools().filter((t) => t.name !== "task" && t.name !== "exit_plan");
|
|
1002
|
+
const unknown = (def.tools ?? []).filter((n) => !base.some((t) => t.name === n));
|
|
1003
|
+
if (unknown.length) return { output: `Agent "${def.name}": alat tidak dikenal: ${unknown.join(", ")}`, isError: true };
|
|
1004
|
+
const allowed = def.tools;
|
|
1005
|
+
const tools = allowed ? base.filter((t) => allowed.includes(t.name)) : base;
|
|
1006
|
+
let provider;
|
|
1007
|
+
try {
|
|
1008
|
+
provider = def.model ? opts.providerFor(def.model) : opts.provider();
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
return { output: `Agent "${def.name}": ${error.message}`, isError: true };
|
|
1011
|
+
}
|
|
1012
|
+
const child = new Agent({
|
|
1013
|
+
provider,
|
|
1014
|
+
tools,
|
|
1015
|
+
permissions: opts.permissions,
|
|
1016
|
+
systemPrompt: `${opts.systemPrompt}
|
|
1017
|
+
|
|
1018
|
+
# Your role
|
|
1019
|
+
${def.prompt}`,
|
|
1020
|
+
cwd: opts.cwd,
|
|
1021
|
+
maxSteps: def.maxSteps ?? 50,
|
|
1022
|
+
label: def.name
|
|
1023
|
+
});
|
|
1024
|
+
let outcome;
|
|
1025
|
+
let steps = 0;
|
|
1026
|
+
child.onEvent = (event) => {
|
|
1027
|
+
if (event.type === "toolStart") steps++;
|
|
1028
|
+
if (event.type === "error" || event.type === "stepLimit" || event.type === "aborted" || event.type === "done") outcome = event;
|
|
1029
|
+
if (event.type !== "usage" && ctx.callId) ctx.emit?.({ type: "subagent", parentId: ctx.callId, agent: def.name, event });
|
|
1030
|
+
};
|
|
1031
|
+
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1032
|
+
await child.run(input.prompt, ctx.signal);
|
|
1033
|
+
ctx.addUsage?.(child.totalUsage);
|
|
1034
|
+
const tokens = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1035
|
+
const display = `${steps} langkah \xB7 ${tokens >= 1e3 ? `${Math.round(tokens / 1e3)}k` : tokens} token`;
|
|
1036
|
+
if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
|
|
1037
|
+
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
|
|
1038
|
+
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
1039
|
+
const report = last && typeof last.content === "string" ? last.content : "(tidak ada laporan)";
|
|
1040
|
+
const suffix = outcome?.type === "stepLimit" ? `
|
|
1041
|
+
|
|
1042
|
+
(Subagent berhenti di batas ${def.maxSteps ?? 50} langkah.)` : "";
|
|
1043
|
+
return { output: report + suffix, display };
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/tools/todo.ts
|
|
1049
|
+
import { z as z13 } from "zod";
|
|
1050
|
+
var TodoStore = class {
|
|
1051
|
+
items = [];
|
|
1052
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1053
|
+
set(items) {
|
|
1054
|
+
this.items = items;
|
|
1055
|
+
for (const fn of this.listeners) fn(items);
|
|
1056
|
+
}
|
|
1057
|
+
subscribe(fn) {
|
|
1058
|
+
this.listeners.add(fn);
|
|
1059
|
+
return () => this.listeners.delete(fn);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
function createTodoTool(store) {
|
|
1063
|
+
return defineTool({
|
|
1064
|
+
name: "todo_write",
|
|
1065
|
+
description: "Write the task list for multi-step work (3+ steps). Send the whole list every time; exactly one item in_progress while working. Mark items completed as soon as they are done.",
|
|
1066
|
+
schema: z13.object({
|
|
1067
|
+
todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
|
|
1068
|
+
}),
|
|
1069
|
+
kind: "read",
|
|
1070
|
+
target: (input) => `${input.todos.length} item`,
|
|
1071
|
+
async run(input) {
|
|
1072
|
+
if (input.todos.filter((t) => t.status === "in_progress").length > 1) {
|
|
1073
|
+
return { output: "Hanya boleh satu item in_progress.", isError: true };
|
|
1074
|
+
}
|
|
1075
|
+
store.set(input.todos);
|
|
1076
|
+
const done = input.todos.filter((t) => t.status === "completed").length;
|
|
1077
|
+
return { output: `Todo diperbarui: ${done}/${input.todos.length} selesai.` };
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1058
1082
|
// src/setup.ts
|
|
1059
1083
|
var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
|
|
1060
1084
|
function createRuntime(opts) {
|
|
1061
1085
|
const env = opts.env ?? process.env;
|
|
1062
1086
|
const home = bccliHome(env);
|
|
1063
|
-
|
|
1087
|
+
let config = loadConfig(opts.cwd, env);
|
|
1064
1088
|
let modelRef = opts.args.model ?? config.model;
|
|
1065
1089
|
const makeProvider = (ref) => {
|
|
1066
1090
|
const resolved = resolveModel(config, ref, env);
|
|
@@ -1070,6 +1094,12 @@ function createRuntime(opts) {
|
|
|
1070
1094
|
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
1071
1095
|
const rules = [...config.allow, ...opts.args.allowedTools.map((t) => TOOL_RULES[t] ?? t)];
|
|
1072
1096
|
const permissions = new Permissions(mode, rules, opts.cwd);
|
|
1097
|
+
const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
|
|
1098
|
+
const skills = loadSkills(roots);
|
|
1099
|
+
const commands = loadCommands(roots);
|
|
1100
|
+
const agentDefs = loadAgentDefs(roots);
|
|
1101
|
+
const todos = new TodoStore();
|
|
1102
|
+
const interaction = { approvePlan: async () => "no" };
|
|
1073
1103
|
let session = Session.create(home, opts.cwd);
|
|
1074
1104
|
let history = opts.history;
|
|
1075
1105
|
if (!history && opts.args.continue) {
|
|
@@ -1079,20 +1109,61 @@ function createRuntime(opts) {
|
|
|
1079
1109
|
history = latest.load();
|
|
1080
1110
|
}
|
|
1081
1111
|
}
|
|
1112
|
+
const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
|
|
1113
|
+
const baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
|
|
1114
|
+
const task = createTaskTool({
|
|
1115
|
+
agents: [...BUILTIN_AGENTS, ...agentDefs],
|
|
1116
|
+
// Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
|
|
1117
|
+
baseTools: () => agent.tools.filter((t) => t.name !== "task"),
|
|
1118
|
+
permissions,
|
|
1119
|
+
provider: () => agent.provider,
|
|
1120
|
+
providerFor: makeProvider,
|
|
1121
|
+
systemPrompt,
|
|
1122
|
+
cwd: opts.cwd
|
|
1123
|
+
});
|
|
1082
1124
|
const agent = new Agent({
|
|
1083
1125
|
provider,
|
|
1084
|
-
tools:
|
|
1126
|
+
tools: [...baseTools, task],
|
|
1085
1127
|
permissions,
|
|
1086
|
-
systemPrompt
|
|
1128
|
+
systemPrompt,
|
|
1087
1129
|
cwd: opts.cwd,
|
|
1088
1130
|
history,
|
|
1089
1131
|
onMessage: (m) => session.append(m),
|
|
1090
1132
|
onReset: () => session.reset()
|
|
1091
1133
|
});
|
|
1134
|
+
const mcp = new McpManager({
|
|
1135
|
+
onChange: () => agent.setTools([...baseTools, task, ...mcp.tools()]),
|
|
1136
|
+
// A later server may reuse a sanitized name; it must not inherit this one's "[a]" grants.
|
|
1137
|
+
onToolsRemoved: (names) => {
|
|
1138
|
+
for (const name of names) permissions.revoke(`mcp(${name})`);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1092
1141
|
return {
|
|
1093
1142
|
cwd: opts.cwd,
|
|
1094
1143
|
home,
|
|
1095
|
-
|
|
1144
|
+
env,
|
|
1145
|
+
skills,
|
|
1146
|
+
commands,
|
|
1147
|
+
agentDefs,
|
|
1148
|
+
todos,
|
|
1149
|
+
interaction,
|
|
1150
|
+
mcp,
|
|
1151
|
+
startMcp: (specs) => mcp.start(specs),
|
|
1152
|
+
get config() {
|
|
1153
|
+
return config;
|
|
1154
|
+
},
|
|
1155
|
+
reloadConfig() {
|
|
1156
|
+
config = loadConfig(opts.cwd, env);
|
|
1157
|
+
},
|
|
1158
|
+
providerLabel() {
|
|
1159
|
+
return providerName(config, modelRef.slice(0, modelRef.indexOf("/")));
|
|
1160
|
+
},
|
|
1161
|
+
listModels: (only) => listAllModels(config, env, { fetch: opts.fetch, only }),
|
|
1162
|
+
addProviderKey(id, key) {
|
|
1163
|
+
return addProviderKey(id, key, { env, cwd: opts.cwd, out: () => {
|
|
1164
|
+
}, err: () => {
|
|
1165
|
+
}, readSecret: async () => "", fetch: opts.fetch });
|
|
1166
|
+
},
|
|
1096
1167
|
get modelRef() {
|
|
1097
1168
|
return modelRef;
|
|
1098
1169
|
},
|
|
@@ -1112,7 +1183,7 @@ function createRuntime(opts) {
|
|
|
1112
1183
|
}
|
|
1113
1184
|
|
|
1114
1185
|
// src/version.ts
|
|
1115
|
-
var VERSION = true ? "0.
|
|
1186
|
+
var VERSION = true ? "0.3.0-beta.2" : "0.0.0-dev";
|
|
1116
1187
|
|
|
1117
1188
|
// src/cli.ts
|
|
1118
1189
|
async function main() {
|
|
@@ -1127,6 +1198,16 @@ async function main() {
|
|
|
1127
1198
|
}
|
|
1128
1199
|
const cwd = process.cwd();
|
|
1129
1200
|
if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
|
|
1201
|
+
if (args.command === "mcp") {
|
|
1202
|
+
const { runMcpCommand } = await import("./mcpCli-BV3IGR47.js");
|
|
1203
|
+
const { readSecret } = await import("./login-IXJD5O7Z.js");
|
|
1204
|
+
return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1205
|
+
}
|
|
1206
|
+
if (args.command === "provider") {
|
|
1207
|
+
const { runProviderCommand } = await import("./providerCli-7QNAYIFT.js");
|
|
1208
|
+
const { readSecret } = await import("./login-IXJD5O7Z.js");
|
|
1209
|
+
return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1210
|
+
}
|
|
1130
1211
|
if (args.command === "models") {
|
|
1131
1212
|
const config = loadConfig(cwd);
|
|
1132
1213
|
const resolved = resolveModel(config, args.model ?? config.model);
|
|
@@ -1141,10 +1222,20 @@ async function main() {
|
|
|
1141
1222
|
}
|
|
1142
1223
|
if (args.print) {
|
|
1143
1224
|
if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
|
|
1144
|
-
|
|
1225
|
+
const { serversToStart } = await import("./config-XPWPGNZJ.js");
|
|
1226
|
+
const plan = serversToStart(rt.home, cwd);
|
|
1227
|
+
for (const s of plan.needTrust) {
|
|
1228
|
+
console.error(`Server MCP project "${s.name}" dilewati (belum diizinkan; jalankan bccli interaktif sekali untuk menyetujuinya).`);
|
|
1229
|
+
}
|
|
1230
|
+
await rt.startMcp(plan.start);
|
|
1231
|
+
try {
|
|
1232
|
+
return await runPrint(rt, args.prompt, void 0, { allowAll: args.allowAll });
|
|
1233
|
+
} finally {
|
|
1234
|
+
await rt.mcp.stop();
|
|
1235
|
+
}
|
|
1145
1236
|
}
|
|
1146
1237
|
if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
|
|
1147
|
-
const { startInteractive } = await import("./ui-
|
|
1238
|
+
const { startInteractive } = await import("./ui-HNIC4DQ6.js");
|
|
1148
1239
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1149
1240
|
}
|
|
1150
1241
|
main().then(
|