@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 +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 +457 -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-6PGC4T6N.js +833 -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,7 +708,12 @@ 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);
|
|
980
718
|
decision = answer === "no" ? "deny" : "allow";
|
|
981
719
|
}
|
|
@@ -995,13 +733,142 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
995
733
|
}
|
|
996
734
|
};
|
|
997
735
|
|
|
736
|
+
// src/agents.ts
|
|
737
|
+
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.";
|
|
738
|
+
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.";
|
|
739
|
+
var BUILTIN_AGENTS = [
|
|
740
|
+
{
|
|
741
|
+
name: "explore",
|
|
742
|
+
description: "Read-only: search and read code to answer a question (fast, safe, can run in parallel).",
|
|
743
|
+
tools: ["read", "grep", "glob"],
|
|
744
|
+
prompt: EXPLORE_PROMPT,
|
|
745
|
+
maxSteps: 30
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
name: "general",
|
|
749
|
+
description: "Full tools: carry out a self-contained sub-task (edits and commands still need user permission).",
|
|
750
|
+
prompt: GENERAL_PROMPT,
|
|
751
|
+
maxSteps: 50
|
|
752
|
+
}
|
|
753
|
+
];
|
|
754
|
+
|
|
755
|
+
// src/mcp/manager.ts
|
|
756
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
757
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
758
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
759
|
+
import { z as z9 } from "zod";
|
|
760
|
+
function mcpToolName(server, tool, taken) {
|
|
761
|
+
const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
762
|
+
const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
|
|
763
|
+
let name = base;
|
|
764
|
+
for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
|
|
765
|
+
taken.add(name);
|
|
766
|
+
return name;
|
|
767
|
+
}
|
|
768
|
+
function withTimeout(promise, ms) {
|
|
769
|
+
let timer;
|
|
770
|
+
const timeout = new Promise((_, reject) => {
|
|
771
|
+
timer = setTimeout(() => reject(new Error(`timeout ${Math.round(ms / 1e3)} detik`)), ms);
|
|
772
|
+
});
|
|
773
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
774
|
+
}
|
|
775
|
+
var McpManager = class {
|
|
776
|
+
constructor(opts = {}) {
|
|
777
|
+
this.opts = opts;
|
|
778
|
+
}
|
|
779
|
+
opts;
|
|
780
|
+
servers = /* @__PURE__ */ new Map();
|
|
781
|
+
taken = /* @__PURE__ */ new Set();
|
|
782
|
+
async start(specs) {
|
|
783
|
+
await Promise.all(specs.map((spec) => this.add(spec)));
|
|
784
|
+
}
|
|
785
|
+
async add(spec) {
|
|
786
|
+
await this.remove(spec.name);
|
|
787
|
+
const entry = { spec, state: { name: spec.name, source: spec.source, status: "starting", tools: 0 }, tools: [] };
|
|
788
|
+
this.servers.set(spec.name, entry);
|
|
789
|
+
this.opts.onChange?.();
|
|
790
|
+
const client = new Client({ name: "bccli", version: "0.3.0" });
|
|
791
|
+
entry.client = client;
|
|
792
|
+
const timeoutMs = this.opts.connectTimeoutMs ?? 3e4;
|
|
793
|
+
try {
|
|
794
|
+
const config = spec.config;
|
|
795
|
+
const transport = config.type === "http" ? new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers: config.headers ?? {} } }) : new StdioClientTransport({
|
|
796
|
+
command: config.command,
|
|
797
|
+
args: config.args ?? [],
|
|
798
|
+
env: { ...process.env, ...config.env },
|
|
799
|
+
stderr: "ignore"
|
|
800
|
+
});
|
|
801
|
+
await withTimeout(client.connect(transport), timeoutMs);
|
|
802
|
+
const { tools } = await withTimeout(client.listTools(), timeoutMs);
|
|
803
|
+
if (this.servers.get(spec.name) !== entry) {
|
|
804
|
+
await client.close().catch(() => {
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
entry.tools = tools.map((t) => this.wrap(spec.name, client, t));
|
|
809
|
+
entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
|
|
810
|
+
} catch (error) {
|
|
811
|
+
entry.state = { ...entry.state, status: "error", error: error.message };
|
|
812
|
+
await client.close().catch(() => {
|
|
813
|
+
});
|
|
814
|
+
if (this.servers.get(spec.name) !== entry) return;
|
|
815
|
+
}
|
|
816
|
+
this.opts.onChange?.();
|
|
817
|
+
}
|
|
818
|
+
wrap(server, client, tool) {
|
|
819
|
+
return {
|
|
820
|
+
name: mcpToolName(server, tool.name, this.taken),
|
|
821
|
+
description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
|
|
822
|
+
schema: z9.record(z9.string(), z9.unknown()),
|
|
823
|
+
jsonSchema: tool.inputSchema,
|
|
824
|
+
kind: "mcp",
|
|
825
|
+
target: (input) => {
|
|
826
|
+
const text = JSON.stringify(input);
|
|
827
|
+
return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
828
|
+
},
|
|
829
|
+
async run(input, ctx) {
|
|
830
|
+
try {
|
|
831
|
+
const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
|
|
832
|
+
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(kosong)";
|
|
833
|
+
return { output: output.length > 5e4 ? `${output.slice(0, 5e4)}
|
|
834
|
+
\u2026 [dipotong]` : output, isError: !!result.isError };
|
|
835
|
+
} catch (error) {
|
|
836
|
+
return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async remove(name) {
|
|
842
|
+
const entry = this.servers.get(name);
|
|
843
|
+
if (!entry) return;
|
|
844
|
+
this.servers.delete(name);
|
|
845
|
+
for (const t of entry.tools) this.taken.delete(t.name);
|
|
846
|
+
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t) => t.name));
|
|
847
|
+
await entry.client?.close().catch(() => {
|
|
848
|
+
});
|
|
849
|
+
this.opts.onChange?.();
|
|
850
|
+
}
|
|
851
|
+
tools() {
|
|
852
|
+
return [...this.servers.values()].flatMap((s) => s.tools);
|
|
853
|
+
}
|
|
854
|
+
states() {
|
|
855
|
+
return [...this.servers.values()].map((s) => s.state);
|
|
856
|
+
}
|
|
857
|
+
async stop() {
|
|
858
|
+
await Promise.all([...this.servers.keys()].map((name) => this.remove(name)));
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// src/setup.ts
|
|
863
|
+
import { homedir } from "os";
|
|
864
|
+
|
|
998
865
|
// src/context.ts
|
|
999
|
-
import { existsSync as
|
|
1000
|
-
import { dirname as dirname2, join
|
|
866
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
867
|
+
import { dirname as dirname2, join } from "path";
|
|
1001
868
|
var FILES = ["AGENTS.md", "BCCLI.md"];
|
|
1002
869
|
function readIf(path) {
|
|
1003
870
|
try {
|
|
1004
|
-
return
|
|
871
|
+
return existsSync2(path) ? readFileSync(path, "utf8").trim() : void 0;
|
|
1005
872
|
} catch {
|
|
1006
873
|
return void 0;
|
|
1007
874
|
}
|
|
@@ -1013,13 +880,13 @@ function loadInstructions(cwd, home) {
|
|
|
1013
880
|
if (dirname2(dir) === dir) break;
|
|
1014
881
|
}
|
|
1015
882
|
const parts = [];
|
|
1016
|
-
const global = readIf(
|
|
1017
|
-
if (global) parts.push(`# ${
|
|
883
|
+
const global = readIf(join(home, "BCCLI.md"));
|
|
884
|
+
if (global) parts.push(`# ${join(home, "BCCLI.md")}
|
|
1018
885
|
${global}`);
|
|
1019
886
|
for (const dir of dirs) {
|
|
1020
887
|
for (const name of FILES) {
|
|
1021
|
-
const text = readIf(
|
|
1022
|
-
if (text) parts.push(`# ${
|
|
888
|
+
const text = readIf(join(dir, name));
|
|
889
|
+
if (text) parts.push(`# ${join(dir, name)}
|
|
1023
890
|
${text}`);
|
|
1024
891
|
}
|
|
1025
892
|
}
|
|
@@ -1027,17 +894,24 @@ ${text}`);
|
|
|
1027
894
|
}
|
|
1028
895
|
function isGitRepo(cwd) {
|
|
1029
896
|
for (let dir = cwd; ; dir = dirname2(dir)) {
|
|
1030
|
-
if (
|
|
897
|
+
if (existsSync2(join(dir, ".git"))) return true;
|
|
1031
898
|
if (dirname2(dir) === dir) return false;
|
|
1032
899
|
}
|
|
1033
900
|
}
|
|
1034
901
|
function buildSystemPrompt(opts) {
|
|
1035
902
|
const instructions = loadInstructions(opts.cwd, opts.home);
|
|
903
|
+
const skills = (opts.skills ?? []).slice(0, 50);
|
|
904
|
+
const skillText = skills.length ? `
|
|
905
|
+
|
|
906
|
+
Skills (load one with the skill tool when it matches the task):
|
|
907
|
+
${skills.map((s) => `- ${s.name}: ${s.description.slice(0, 200)}`).join("\n")}` : "";
|
|
1036
908
|
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
909
|
|
|
1038
910
|
How to work:
|
|
1039
911
|
- Understand before changing: read the relevant files and search the codebase first. Never guess file contents.
|
|
1040
912
|
- Always read a file before editing it. Make the smallest correct change; match the existing style.
|
|
913
|
+
- For tasks with 3 or more steps, keep a todo list with todo_write and update it as you go.
|
|
914
|
+
- In plan mode you can only read and search. When the plan is ready, call exit_plan with it and wait for approval.
|
|
1041
915
|
- Verify your work: run the tests, build or the command that proves the change works, and report the real result.
|
|
1042
916
|
- The user must approve edits and commands. If they decline, ask what they want instead of retrying.
|
|
1043
917
|
- Be concise. Reply in the user's language. No preamble; lead with the answer or the action.
|
|
@@ -1048,19 +922,168 @@ Environment:
|
|
|
1048
922
|
- Git repository: ${isGitRepo(opts.cwd) ? "yes" : "no"}
|
|
1049
923
|
- Platform: ${opts.platform ?? process.platform}
|
|
1050
924
|
- Date: ${opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
1051
|
-
- Model: ${opts.model}${instructions ? `
|
|
925
|
+
- Model: ${opts.model}${skillText}${instructions ? `
|
|
1052
926
|
|
|
1053
927
|
Project and user instructions (follow them):
|
|
1054
928
|
|
|
1055
929
|
${instructions}` : ""}`;
|
|
1056
930
|
}
|
|
1057
931
|
|
|
932
|
+
// src/tools/plan.ts
|
|
933
|
+
import { z as z10 } from "zod";
|
|
934
|
+
function createExitPlanTool(opts) {
|
|
935
|
+
return defineTool({
|
|
936
|
+
name: "exit_plan",
|
|
937
|
+
description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
|
|
938
|
+
schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
|
|
939
|
+
kind: "read",
|
|
940
|
+
target: () => "rencana",
|
|
941
|
+
async run(input) {
|
|
942
|
+
if (opts.permissions.mode !== "plan") return { output: "exit_plan hanya dipakai di mode plan.", isError: true };
|
|
943
|
+
const decision = await opts.interaction.approvePlan(input.plan);
|
|
944
|
+
if (decision === "no") {
|
|
945
|
+
return { output: "User belum menyetujui. Tanyakan apa yang perlu diperbaiki, perbaiki rencananya, lalu panggil exit_plan lagi." };
|
|
946
|
+
}
|
|
947
|
+
opts.permissions.mode = decision;
|
|
948
|
+
return { output: `Rencana disetujui. Mode sekarang ${decision}. Mulai kerjakan rencananya sekarang.` };
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/tools/skill.ts
|
|
954
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
955
|
+
import { z as z11 } from "zod";
|
|
956
|
+
function createSkillTool(skills) {
|
|
957
|
+
return defineTool({
|
|
958
|
+
name: "skill",
|
|
959
|
+
description: "Load a skill (instructions for a kind of task) by name. Use it when a listed skill matches the task, then follow it.",
|
|
960
|
+
schema: z11.object({ name: z11.string().describe("Skill name from the list in the system prompt") }),
|
|
961
|
+
kind: "read",
|
|
962
|
+
target: (input) => input.name,
|
|
963
|
+
async run(input) {
|
|
964
|
+
const skill = skills.find((s) => s.name === input.name);
|
|
965
|
+
if (!skill) return { output: `Skill "${input.name}" tidak ada. Tersedia: ${skills.map((s) => s.name).join(", ") || "(tidak ada)"}`, isError: true };
|
|
966
|
+
const { body } = parseFrontmatter(readFileSync2(skill.file, "utf8"));
|
|
967
|
+
return { output: `Base directory for this skill: ${skill.dir}
|
|
968
|
+
|
|
969
|
+
${body}`, display: skill.description.slice(0, 60) };
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/tools/task.ts
|
|
975
|
+
import { z as z12 } from "zod";
|
|
976
|
+
function createTaskTool(opts) {
|
|
977
|
+
const byName = new Map(opts.agents.map((a) => [a.name, a]));
|
|
978
|
+
const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
979
|
+
return defineTool({
|
|
980
|
+
name: "task",
|
|
981
|
+
description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
|
|
982
|
+
${list}`,
|
|
983
|
+
schema: z12.object({
|
|
984
|
+
agent: z12.string().describe("Agent name"),
|
|
985
|
+
description: z12.string().describe("3-6 word summary shown to the user"),
|
|
986
|
+
prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
|
|
987
|
+
}),
|
|
988
|
+
kind: "read",
|
|
989
|
+
target: (input) => `[${input.agent}] ${input.description}`,
|
|
990
|
+
// Parallel only when the resolved agent can do nothing but read (a project file may redefine "explore").
|
|
991
|
+
parallelSafe: (input) => {
|
|
992
|
+
const allowed = byName.get(input.agent)?.tools;
|
|
993
|
+
if (!allowed) return false;
|
|
994
|
+
const base = opts.baseTools();
|
|
995
|
+
return allowed.every((name) => base.find((t) => t.name === name)?.kind === "read");
|
|
996
|
+
},
|
|
997
|
+
async run(input, ctx) {
|
|
998
|
+
const def = byName.get(input.agent);
|
|
999
|
+
if (!def) return { output: `Agent "${input.agent}" tidak ada. Tersedia: ${[...byName.keys()].join(", ")}`, isError: true };
|
|
1000
|
+
const base = opts.baseTools().filter((t) => t.name !== "task" && t.name !== "exit_plan");
|
|
1001
|
+
const unknown = (def.tools ?? []).filter((n) => !base.some((t) => t.name === n));
|
|
1002
|
+
if (unknown.length) return { output: `Agent "${def.name}": alat tidak dikenal: ${unknown.join(", ")}`, isError: true };
|
|
1003
|
+
const allowed = def.tools;
|
|
1004
|
+
const tools = allowed ? base.filter((t) => allowed.includes(t.name)) : base;
|
|
1005
|
+
let provider;
|
|
1006
|
+
try {
|
|
1007
|
+
provider = def.model ? opts.providerFor(def.model) : opts.provider();
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
return { output: `Agent "${def.name}": ${error.message}`, isError: true };
|
|
1010
|
+
}
|
|
1011
|
+
const child = new Agent({
|
|
1012
|
+
provider,
|
|
1013
|
+
tools,
|
|
1014
|
+
permissions: opts.permissions,
|
|
1015
|
+
systemPrompt: `${opts.systemPrompt}
|
|
1016
|
+
|
|
1017
|
+
# Your role
|
|
1018
|
+
${def.prompt}`,
|
|
1019
|
+
cwd: opts.cwd,
|
|
1020
|
+
maxSteps: def.maxSteps ?? 50,
|
|
1021
|
+
label: def.name
|
|
1022
|
+
});
|
|
1023
|
+
let outcome;
|
|
1024
|
+
let steps = 0;
|
|
1025
|
+
child.onEvent = (event) => {
|
|
1026
|
+
if (event.type === "toolStart") steps++;
|
|
1027
|
+
if (event.type === "error" || event.type === "stepLimit" || event.type === "aborted" || event.type === "done") outcome = event;
|
|
1028
|
+
if (event.type !== "usage" && ctx.callId) ctx.emit?.({ type: "subagent", parentId: ctx.callId, agent: def.name, event });
|
|
1029
|
+
};
|
|
1030
|
+
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1031
|
+
await child.run(input.prompt, ctx.signal);
|
|
1032
|
+
ctx.addUsage?.(child.totalUsage);
|
|
1033
|
+
const tokens = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1034
|
+
const display = `${steps} langkah \xB7 ${tokens >= 1e3 ? `${Math.round(tokens / 1e3)}k` : tokens} token`;
|
|
1035
|
+
if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
|
|
1036
|
+
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
|
|
1037
|
+
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
1038
|
+
const report = last && typeof last.content === "string" ? last.content : "(tidak ada laporan)";
|
|
1039
|
+
const suffix = outcome?.type === "stepLimit" ? `
|
|
1040
|
+
|
|
1041
|
+
(Subagent berhenti di batas ${def.maxSteps ?? 50} langkah.)` : "";
|
|
1042
|
+
return { output: report + suffix, display };
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/tools/todo.ts
|
|
1048
|
+
import { z as z13 } from "zod";
|
|
1049
|
+
var TodoStore = class {
|
|
1050
|
+
items = [];
|
|
1051
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1052
|
+
set(items) {
|
|
1053
|
+
this.items = items;
|
|
1054
|
+
for (const fn of this.listeners) fn(items);
|
|
1055
|
+
}
|
|
1056
|
+
subscribe(fn) {
|
|
1057
|
+
this.listeners.add(fn);
|
|
1058
|
+
return () => this.listeners.delete(fn);
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
function createTodoTool(store) {
|
|
1062
|
+
return defineTool({
|
|
1063
|
+
name: "todo_write",
|
|
1064
|
+
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.",
|
|
1065
|
+
schema: z13.object({
|
|
1066
|
+
todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
|
|
1067
|
+
}),
|
|
1068
|
+
kind: "read",
|
|
1069
|
+
target: (input) => `${input.todos.length} item`,
|
|
1070
|
+
async run(input) {
|
|
1071
|
+
if (input.todos.filter((t) => t.status === "in_progress").length > 1) {
|
|
1072
|
+
return { output: "Hanya boleh satu item in_progress.", isError: true };
|
|
1073
|
+
}
|
|
1074
|
+
store.set(input.todos);
|
|
1075
|
+
const done = input.todos.filter((t) => t.status === "completed").length;
|
|
1076
|
+
return { output: `Todo diperbarui: ${done}/${input.todos.length} selesai.` };
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1058
1081
|
// src/setup.ts
|
|
1059
1082
|
var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
|
|
1060
1083
|
function createRuntime(opts) {
|
|
1061
1084
|
const env = opts.env ?? process.env;
|
|
1062
1085
|
const home = bccliHome(env);
|
|
1063
|
-
|
|
1086
|
+
let config = loadConfig(opts.cwd, env);
|
|
1064
1087
|
let modelRef = opts.args.model ?? config.model;
|
|
1065
1088
|
const makeProvider = (ref) => {
|
|
1066
1089
|
const resolved = resolveModel(config, ref, env);
|
|
@@ -1070,6 +1093,12 @@ function createRuntime(opts) {
|
|
|
1070
1093
|
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
1071
1094
|
const rules = [...config.allow, ...opts.args.allowedTools.map((t) => TOOL_RULES[t] ?? t)];
|
|
1072
1095
|
const permissions = new Permissions(mode, rules, opts.cwd);
|
|
1096
|
+
const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
|
|
1097
|
+
const skills = loadSkills(roots);
|
|
1098
|
+
const commands = loadCommands(roots);
|
|
1099
|
+
const agentDefs = loadAgentDefs(roots);
|
|
1100
|
+
const todos = new TodoStore();
|
|
1101
|
+
const interaction = { approvePlan: async () => "no" };
|
|
1073
1102
|
let session = Session.create(home, opts.cwd);
|
|
1074
1103
|
let history = opts.history;
|
|
1075
1104
|
if (!history && opts.args.continue) {
|
|
@@ -1079,20 +1108,61 @@ function createRuntime(opts) {
|
|
|
1079
1108
|
history = latest.load();
|
|
1080
1109
|
}
|
|
1081
1110
|
}
|
|
1111
|
+
const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
|
|
1112
|
+
const baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
|
|
1113
|
+
const task = createTaskTool({
|
|
1114
|
+
agents: [...BUILTIN_AGENTS, ...agentDefs],
|
|
1115
|
+
// Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
|
|
1116
|
+
baseTools: () => agent.tools.filter((t) => t.name !== "task"),
|
|
1117
|
+
permissions,
|
|
1118
|
+
provider: () => agent.provider,
|
|
1119
|
+
providerFor: makeProvider,
|
|
1120
|
+
systemPrompt,
|
|
1121
|
+
cwd: opts.cwd
|
|
1122
|
+
});
|
|
1082
1123
|
const agent = new Agent({
|
|
1083
1124
|
provider,
|
|
1084
|
-
tools:
|
|
1125
|
+
tools: [...baseTools, task],
|
|
1085
1126
|
permissions,
|
|
1086
|
-
systemPrompt
|
|
1127
|
+
systemPrompt,
|
|
1087
1128
|
cwd: opts.cwd,
|
|
1088
1129
|
history,
|
|
1089
1130
|
onMessage: (m) => session.append(m),
|
|
1090
1131
|
onReset: () => session.reset()
|
|
1091
1132
|
});
|
|
1133
|
+
const mcp = new McpManager({
|
|
1134
|
+
onChange: () => agent.setTools([...baseTools, task, ...mcp.tools()]),
|
|
1135
|
+
// A later server may reuse a sanitized name; it must not inherit this one's "[a]" grants.
|
|
1136
|
+
onToolsRemoved: (names) => {
|
|
1137
|
+
for (const name of names) permissions.revoke(`mcp(${name})`);
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1092
1140
|
return {
|
|
1093
1141
|
cwd: opts.cwd,
|
|
1094
1142
|
home,
|
|
1095
|
-
|
|
1143
|
+
env,
|
|
1144
|
+
skills,
|
|
1145
|
+
commands,
|
|
1146
|
+
agentDefs,
|
|
1147
|
+
todos,
|
|
1148
|
+
interaction,
|
|
1149
|
+
mcp,
|
|
1150
|
+
startMcp: (specs) => mcp.start(specs),
|
|
1151
|
+
get config() {
|
|
1152
|
+
return config;
|
|
1153
|
+
},
|
|
1154
|
+
reloadConfig() {
|
|
1155
|
+
config = loadConfig(opts.cwd, env);
|
|
1156
|
+
},
|
|
1157
|
+
providerLabel() {
|
|
1158
|
+
return providerName(config, modelRef.slice(0, modelRef.indexOf("/")));
|
|
1159
|
+
},
|
|
1160
|
+
listModels: (only) => listAllModels(config, env, { fetch: opts.fetch, only }),
|
|
1161
|
+
addProviderKey(id, key) {
|
|
1162
|
+
return addProviderKey(id, key, { env, cwd: opts.cwd, out: () => {
|
|
1163
|
+
}, err: () => {
|
|
1164
|
+
}, readSecret: async () => "", fetch: opts.fetch });
|
|
1165
|
+
},
|
|
1096
1166
|
get modelRef() {
|
|
1097
1167
|
return modelRef;
|
|
1098
1168
|
},
|
|
@@ -1112,7 +1182,7 @@ function createRuntime(opts) {
|
|
|
1112
1182
|
}
|
|
1113
1183
|
|
|
1114
1184
|
// src/version.ts
|
|
1115
|
-
var VERSION = true ? "0.
|
|
1185
|
+
var VERSION = true ? "0.3.0-beta.1" : "0.0.0-dev";
|
|
1116
1186
|
|
|
1117
1187
|
// src/cli.ts
|
|
1118
1188
|
async function main() {
|
|
@@ -1127,6 +1197,16 @@ async function main() {
|
|
|
1127
1197
|
}
|
|
1128
1198
|
const cwd = process.cwd();
|
|
1129
1199
|
if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
|
|
1200
|
+
if (args.command === "mcp") {
|
|
1201
|
+
const { runMcpCommand } = await import("./mcpCli-BV3IGR47.js");
|
|
1202
|
+
const { readSecret } = await import("./login-IXJD5O7Z.js");
|
|
1203
|
+
return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1204
|
+
}
|
|
1205
|
+
if (args.command === "provider") {
|
|
1206
|
+
const { runProviderCommand } = await import("./providerCli-7QNAYIFT.js");
|
|
1207
|
+
const { readSecret } = await import("./login-IXJD5O7Z.js");
|
|
1208
|
+
return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1209
|
+
}
|
|
1130
1210
|
if (args.command === "models") {
|
|
1131
1211
|
const config = loadConfig(cwd);
|
|
1132
1212
|
const resolved = resolveModel(config, args.model ?? config.model);
|
|
@@ -1141,10 +1221,20 @@ async function main() {
|
|
|
1141
1221
|
}
|
|
1142
1222
|
if (args.print) {
|
|
1143
1223
|
if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
|
|
1144
|
-
|
|
1224
|
+
const { serversToStart } = await import("./config-XPWPGNZJ.js");
|
|
1225
|
+
const plan = serversToStart(rt.home, cwd);
|
|
1226
|
+
for (const s of plan.needTrust) {
|
|
1227
|
+
console.error(`Server MCP project "${s.name}" dilewati (belum diizinkan; jalankan bccli interaktif sekali untuk menyetujuinya).`);
|
|
1228
|
+
}
|
|
1229
|
+
await rt.startMcp(plan.start);
|
|
1230
|
+
try {
|
|
1231
|
+
return await runPrint(rt, args.prompt, void 0, { allowAll: args.allowAll });
|
|
1232
|
+
} finally {
|
|
1233
|
+
await rt.mcp.stop();
|
|
1234
|
+
}
|
|
1145
1235
|
}
|
|
1146
1236
|
if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
|
|
1147
|
-
const { startInteractive } = await import("./ui-
|
|
1237
|
+
const { startInteractive } = await import("./ui-6PGC4T6N.js");
|
|
1148
1238
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1149
1239
|
}
|
|
1150
1240
|
main().then(
|