@botconnector/bccli 0.1.0-beta.3 → 0.2.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/dist/cli.js ADDED
@@ -0,0 +1,1156 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Session,
4
+ pruneSessions
5
+ } from "./chunk-SJH3YLW4.js";
6
+ import {
7
+ MODE_ORDER,
8
+ Permissions,
9
+ ignorePatterns,
10
+ runPrint
11
+ } from "./chunk-WZYWKWPD.js";
12
+
13
+ // src/args.ts
14
+ import { parseArgs } from "util";
15
+
16
+ // src/config.ts
17
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
18
+ import { homedir } from "os";
19
+ import { join } from "path";
20
+ var ConfigError = class extends Error {
21
+ };
22
+ var DEFAULT_CONFIG = {
23
+ model: "bc-cloud/glm-5.3-flash",
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
+ }
344
+
345
+ // src/tools/index.ts
346
+ import { z as z8 } from "zod";
347
+
348
+ // src/tools/bash.ts
349
+ import { spawn } from "child_process";
350
+ import { z } from "zod";
351
+
352
+ // src/tools/types.ts
353
+ function defineTool(tool) {
354
+ return tool;
355
+ }
356
+
357
+ // src/tools/bash.ts
358
+ var HALF = 15e3;
359
+ function killProcessTree(pid, platform = process.platform, spawnFn = spawn) {
360
+ if (platform === "win32") {
361
+ const killer = spawnFn("taskkill", ["/pid", String(pid), "/T", "/F"]);
362
+ killer.on("error", () => {
363
+ try {
364
+ process.kill(pid);
365
+ } catch {
366
+ }
367
+ });
368
+ return;
369
+ }
370
+ try {
371
+ process.kill(-pid, "SIGKILL");
372
+ } catch {
373
+ try {
374
+ process.kill(pid, "SIGKILL");
375
+ } catch {
376
+ }
377
+ }
378
+ }
379
+ var active = /* @__PURE__ */ new Set();
380
+ var exitHookInstalled = false;
381
+ function killAllCommands() {
382
+ for (const kill of active) kill();
383
+ }
384
+ function runCommand(command, opts) {
385
+ return new Promise((resolve2) => {
386
+ const child = spawn(command, {
387
+ cwd: opts.cwd,
388
+ shell: true,
389
+ detached: process.platform !== "win32",
390
+ stdio: ["ignore", "pipe", "pipe"]
391
+ });
392
+ if (!exitHookInstalled) {
393
+ process.on("exit", killAllCommands);
394
+ exitHookInstalled = true;
395
+ }
396
+ child.stdout.setEncoding("utf8");
397
+ child.stderr.setEncoding("utf8");
398
+ let head = "";
399
+ let tail = "";
400
+ let dropped = 0;
401
+ let timedOut = false;
402
+ let aborted = false;
403
+ const onData = (data) => {
404
+ let s = data;
405
+ if (head.length < HALF) {
406
+ const take = s.slice(0, HALF - head.length);
407
+ head += take;
408
+ s = s.slice(take.length);
409
+ }
410
+ if (s) {
411
+ tail += s;
412
+ if (tail.length > HALF) {
413
+ dropped += tail.length - HALF;
414
+ tail = tail.slice(-HALF);
415
+ }
416
+ }
417
+ };
418
+ child.stdout.on("data", onData);
419
+ child.stderr.on("data", onData);
420
+ const kill = () => {
421
+ if (child.pid !== void 0) killProcessTree(child.pid);
422
+ };
423
+ const killAsAborted = () => {
424
+ aborted = true;
425
+ kill();
426
+ };
427
+ active.add(killAsAborted);
428
+ const timer = setTimeout(() => {
429
+ timedOut = true;
430
+ kill();
431
+ }, opts.timeoutMs);
432
+ const onAbort = () => {
433
+ aborted = true;
434
+ kill();
435
+ };
436
+ if (opts.signal.aborted) onAbort();
437
+ opts.signal.addEventListener("abort", onAbort, { once: true });
438
+ const finish = (exitCode, extra = "") => {
439
+ active.delete(killAsAborted);
440
+ clearTimeout(timer);
441
+ opts.signal.removeEventListener("abort", onAbort);
442
+ const output = dropped ? `${head}
443
+ \u2026 [${dropped} karakter dipotong] \u2026
444
+ ${tail}` : head + tail;
445
+ resolve2({ output: output + extra, exitCode, timedOut, aborted });
446
+ };
447
+ child.on("error", (error) => finish(null, error.message));
448
+ child.on("close", (code) => finish(code));
449
+ });
450
+ }
451
+ var bashTool = defineTool({
452
+ name: "bash",
453
+ description: "Run a shell command in the project directory. No interactive input. Default timeout 2 minutes (max 10). Use for tests, builds, git, and other CLI tasks.",
454
+ schema: z.object({
455
+ command: z.string().describe("The shell command to run"),
456
+ timeout_ms: z.number().int().min(1e3).max(6e5).optional().describe("Timeout in milliseconds")
457
+ }),
458
+ kind: "bash",
459
+ target: (input) => input.command,
460
+ async run(input, ctx) {
461
+ const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
462
+ const status = r.timedOut ? `[dihentikan: timeout ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} detik]` : r.aborted ? "[dibatalkan user]" : `[exit code ${r.exitCode}]`;
463
+ return { output: `${r.output.trimEnd()}
464
+ ${status}`.trimStart(), isError: r.exitCode !== 0 };
465
+ }
466
+ });
467
+
468
+ // src/tools/edit.ts
469
+ import { readFile, writeFile } from "fs/promises";
470
+ import { z as z2 } from "zod";
471
+
472
+ // src/diff.ts
473
+ import { structuredPatch } from "diff";
474
+ function diffLines(oldText, newText) {
475
+ const patch = structuredPatch("a", "b", oldText, newText, "", "", { context: 2 });
476
+ const out = [];
477
+ for (const hunk of patch.hunks) {
478
+ let oldLine = hunk.oldStart;
479
+ let newLine = hunk.newStart;
480
+ for (const raw of hunk.lines) {
481
+ const text = raw.slice(1);
482
+ if (raw[0] === "-") out.push({ kind: "del", line: oldLine++, text });
483
+ else if (raw[0] === "+") out.push({ kind: "add", line: newLine++, text });
484
+ else if (raw[0] === " ") {
485
+ out.push({ kind: "ctx", line: newLine, text });
486
+ oldLine++;
487
+ newLine++;
488
+ }
489
+ }
490
+ }
491
+ return out;
492
+ }
493
+ function formatDiff(lines) {
494
+ const mark = { add: "+", del: "-", ctx: " " };
495
+ return lines.map((l) => `${String(l.line).padStart(5)} ${mark[l.kind]} ${l.text}`).join("\n");
496
+ }
497
+
498
+ // src/tools/paths.ts
499
+ import { isAbsolute, resolve } from "path";
500
+ function resolvePath(cwd, path) {
501
+ return isAbsolute(path) ? path : resolve(cwd, path);
502
+ }
503
+ function errorCode(error) {
504
+ return error.code ?? error.message;
505
+ }
506
+
507
+ // src/tools/edit.ts
508
+ var schema = z2.object({
509
+ path: z2.string().describe("File path, relative to the project or absolute"),
510
+ old_string: z2.string().describe("Exact text to replace; must be unique unless replace_all"),
511
+ new_string: z2.string().describe("Replacement text"),
512
+ replace_all: z2.boolean().optional().describe("Replace every occurrence")
513
+ });
514
+ function countOccurrences(haystack, needle) {
515
+ let count = 0;
516
+ for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
517
+ return count;
518
+ }
519
+ function applyEdit(content, input) {
520
+ if (input.old_string === input.new_string) {
521
+ return { output: "old_string dan new_string sama; tidak ada perubahan.", isError: true };
522
+ }
523
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
524
+ const toEol = (s) => s.replace(/\r?\n/g, eol);
525
+ const attempts = [[input.old_string, input.new_string]];
526
+ if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
527
+ for (const [oldText, newText] of attempts) {
528
+ const count = oldText ? countOccurrences(content, oldText) : 0;
529
+ if (count === 0) continue;
530
+ if (count > 1 && !input.replace_all) {
531
+ return {
532
+ output: `old_string muncul ${count} kali di ${input.path}. Tambah konteks agar unik, atau pakai replace_all.`,
533
+ isError: true
534
+ };
535
+ }
536
+ return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
537
+ }
538
+ return { output: `old_string tidak ditemukan di ${input.path}. Baca ulang file-nya.`, isError: true };
539
+ }
540
+ var editTool = defineTool({
541
+ name: "edit",
542
+ description: "Replace exact text in a file. The file must have been read first. old_string must match exactly (including indentation) and be unique unless replace_all is true.",
543
+ schema,
544
+ kind: "edit",
545
+ target: (input) => input.path,
546
+ async validate(input, ctx) {
547
+ const abs = resolvePath(ctx.cwd, input.path);
548
+ if (!ctx.readFiles.has(abs)) return `Baca ${input.path} dulu dengan read sebelum mengedit.`;
549
+ let content;
550
+ try {
551
+ content = await readFile(abs, "utf8");
552
+ } catch (error) {
553
+ return `Tidak bisa membaca ${input.path}: ${errorCode(error)}`;
554
+ }
555
+ const next = applyEdit(content, input);
556
+ return typeof next === "string" ? void 0 : next.output;
557
+ },
558
+ async preview(input, ctx) {
559
+ const abs = resolvePath(ctx.cwd, input.path);
560
+ const content = await readFile(abs, "utf8");
561
+ const next = applyEdit(content, input);
562
+ return typeof next === "string" ? formatDiff(diffLines(content, next)) : void 0;
563
+ },
564
+ async run(input, ctx) {
565
+ const abs = resolvePath(ctx.cwd, input.path);
566
+ if (!ctx.readFiles.has(abs)) {
567
+ return { output: `Baca ${input.path} dulu dengan read sebelum mengedit.`, isError: true };
568
+ }
569
+ let content;
570
+ try {
571
+ content = await readFile(abs, "utf8");
572
+ } catch (error) {
573
+ return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
574
+ }
575
+ const next = applyEdit(content, input);
576
+ if (typeof next !== "string") return next;
577
+ await writeFile(abs, next);
578
+ return { output: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
579
+ }
580
+ });
581
+
582
+ // src/tools/fetch.ts
583
+ import { z as z3 } from "zod";
584
+ var MAX_CHARS = 5e4;
585
+ var MAX_BYTES = 5 * 1024 * 1024;
586
+ async function readCapped(res) {
587
+ const reader = res.body?.getReader();
588
+ if (!reader) return { text: "", truncated: false };
589
+ const chunks = [];
590
+ let size = 0;
591
+ for (; ; ) {
592
+ const { done, value } = await reader.read();
593
+ if (done) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
594
+ size += value.length;
595
+ if (size > MAX_BYTES) {
596
+ chunks.push(value.subarray(0, value.length - (size - MAX_BYTES)));
597
+ await reader.cancel();
598
+ return { text: Buffer.concat(chunks).toString("utf8"), truncated: true };
599
+ }
600
+ chunks.push(value);
601
+ }
602
+ }
603
+ function htmlToText(html) {
604
+ return html.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<(br|\/p|\/div|\/h[1-6]|\/li|\/tr)\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
605
+ }
606
+ var fetchTool = defineTool({
607
+ name: "fetch",
608
+ description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
609
+ schema: z3.object({ url: z3.string().describe("Absolute http(s) URL") }),
610
+ kind: "fetch",
611
+ target: (input) => input.url,
612
+ async run(input, ctx) {
613
+ if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
614
+ let url = new URL(input.url);
615
+ let res;
616
+ try {
617
+ for (let hop = 0; hop < 5; hop++) {
618
+ res = await fetch(url, { signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
619
+ const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
620
+ if (!location) break;
621
+ const next = new URL(location, url);
622
+ if (next.host !== url.host) {
623
+ await res.body?.cancel();
624
+ return {
625
+ output: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).`,
626
+ isError: true
627
+ };
628
+ }
629
+ await res.body?.cancel();
630
+ url = next;
631
+ }
632
+ } catch (error) {
633
+ return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
634
+ }
635
+ if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
636
+ const { text: body, truncated } = await readCapped(res);
637
+ if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${body.slice(0, 500)}`, isError: true };
638
+ const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
639
+ const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
640
+ \u2026 [dipotong]` : text;
641
+ const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
642
+ return { output: clipped, display };
643
+ }
644
+ });
645
+
646
+ // src/tools/glob.ts
647
+ import fg from "fast-glob";
648
+ import { z as z4 } from "zod";
649
+ var globTool = defineTool({
650
+ name: "glob",
651
+ description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
652
+ schema: z4.object({
653
+ pattern: z4.string().describe("Glob pattern"),
654
+ path: z4.string().optional().describe("Base directory, default project root")
655
+ }),
656
+ kind: "read",
657
+ target: (input) => input.pattern,
658
+ async run(input, ctx) {
659
+ const root = resolvePath(ctx.cwd, input.path ?? ".");
660
+ const files = (await fg(input.pattern, { cwd: root, ignore: ignorePatterns(root), onlyFiles: true, dot: true })).sort();
661
+ if (!files.length) return { output: "Tidak ada file yang cocok." };
662
+ const shown = files.slice(0, 500);
663
+ const more = files.length > shown.length ? `
664
+ \u2026 ${files.length - shown.length} file lagi` : "";
665
+ return { output: shown.join("\n") + more, display: `${files.length} file` };
666
+ }
667
+ });
668
+
669
+ // src/tools/grep.ts
670
+ import { execFile } from "child_process";
671
+ import { readFile as readFile2 } from "fs/promises";
672
+ import fg2 from "fast-glob";
673
+ import { z as z5 } from "zod";
674
+ var MAX_MATCHES = 200;
675
+ async function grepJs(pattern, opts) {
676
+ const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
677
+ const files = await fg2(opts.glob ?? "**/*", { cwd: opts.root, ignore: ignorePatterns(opts.root), onlyFiles: true, dot: true });
678
+ const out = [];
679
+ for (const file of files.sort()) {
680
+ let text;
681
+ try {
682
+ const buffer = await readFile2(resolvePath(opts.root, file));
683
+ if (buffer.subarray(0, 8e3).includes(0)) continue;
684
+ text = buffer.toString("utf8");
685
+ } catch {
686
+ continue;
687
+ }
688
+ const lines = text.split(/\r?\n/);
689
+ for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
690
+ if (regex.test(lines[i])) out.push(`${file}:${i + 1}:${lines[i].slice(0, 300)}`);
691
+ }
692
+ if (out.length >= MAX_MATCHES) break;
693
+ }
694
+ return out;
695
+ }
696
+ function grepRg(pattern, opts) {
697
+ const args = ["--line-number", "--no-heading", "--color", "never", "--max-columns", "300", "-e", pattern];
698
+ if (opts.ignoreCase) args.push("-i");
699
+ if (opts.glob) args.push("-g", opts.glob);
700
+ args.push(".");
701
+ return new Promise((resolve2) => {
702
+ execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
703
+ const code = error?.code;
704
+ if (code === "ENOENT") return resolve2(void 0);
705
+ if (error && error.code !== 1) return resolve2(void 0);
706
+ resolve2(stdout.split("\n").filter(Boolean).map((l) => l.replace(/^\.\//, "")).slice(0, MAX_MATCHES));
707
+ });
708
+ });
709
+ }
710
+ var grepTool = defineTool({
711
+ name: "grep",
712
+ description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
713
+ schema: z5.object({
714
+ pattern: z5.string().describe("Regular expression"),
715
+ path: z5.string().optional().describe("Directory to search, default project root"),
716
+ glob: z5.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
717
+ ignore_case: z5.boolean().optional()
718
+ }),
719
+ kind: "read",
720
+ target: (input) => input.pattern,
721
+ async run(input, ctx) {
722
+ const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case };
723
+ try {
724
+ new RegExp(input.pattern);
725
+ } catch (error) {
726
+ return { output: `Regex tidak valid: ${error.message}`, isError: true };
727
+ }
728
+ const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
729
+ if (!matches.length) return { output: "Tidak ada yang cocok." };
730
+ return { output: matches.join("\n"), display: `${matches.length} hasil` };
731
+ }
732
+ });
733
+
734
+ // src/tools/read.ts
735
+ import { readFile as readFile3 } from "fs/promises";
736
+ import { z as z6 } from "zod";
737
+ var MAX_LINE = 2e3;
738
+ var readTool = defineTool({
739
+ name: "read",
740
+ description: "Read a text file. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it.",
741
+ schema: z6.object({
742
+ path: z6.string().describe("File path, relative to the project or absolute"),
743
+ offset: z6.number().int().min(1).optional().describe("First line to read, 1-based"),
744
+ limit: z6.number().int().min(1).optional().describe("Number of lines, default 2000")
745
+ }),
746
+ kind: "read",
747
+ target: (input) => input.path,
748
+ async run(input, ctx) {
749
+ const abs = resolvePath(ctx.cwd, input.path);
750
+ let buffer;
751
+ try {
752
+ buffer = await readFile3(abs);
753
+ } catch (error) {
754
+ return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
755
+ }
756
+ if (buffer.subarray(0, 8e3).includes(0)) {
757
+ return { output: `${input.path} adalah file biner, tidak dibaca.`, isError: true };
758
+ }
759
+ const lines = buffer.toString("utf8").split(/\r?\n/);
760
+ const start = (input.offset ?? 1) - 1;
761
+ const limit = input.limit ?? 2e3;
762
+ const slice = lines.slice(start, start + limit);
763
+ const body = slice.map((line, i) => {
764
+ const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [baris dipotong]` : line;
765
+ return `${String(start + i + 1).padStart(6)} ${text}`;
766
+ }).join("\n");
767
+ const remaining = lines.length - start - slice.length;
768
+ ctx.readFiles.add(abs);
769
+ return {
770
+ output: remaining > 0 ? `${body}
771
+ \u2026 ${remaining} baris lagi (pakai offset)` : body,
772
+ display: `${slice.length} baris`
773
+ };
774
+ }
775
+ });
776
+
777
+ // src/tools/write.ts
778
+ import { existsSync as existsSync2 } from "fs";
779
+ import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
780
+ import { dirname } from "path";
781
+ import { z as z7 } from "zod";
782
+ var writeTool = defineTool({
783
+ name: "write",
784
+ description: "Create or overwrite a file with the given content. Read an existing file first. Prefer edit for changes.",
785
+ schema: z7.object({
786
+ path: z7.string().describe("File path, relative to the project or absolute"),
787
+ content: z7.string().describe("Full new file content")
788
+ }),
789
+ kind: "edit",
790
+ target: (input) => input.path,
791
+ async validate(input, ctx) {
792
+ const abs = resolvePath(ctx.cwd, input.path);
793
+ return existsSync2(abs) && !ctx.readFiles.has(abs) ? `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.` : void 0;
794
+ },
795
+ async preview(input, ctx) {
796
+ const abs = resolvePath(ctx.cwd, input.path);
797
+ const old = existsSync2(abs) ? await readFile4(abs, "utf8") : "";
798
+ return formatDiff(diffLines(old, input.content));
799
+ },
800
+ async run(input, ctx) {
801
+ const abs = resolvePath(ctx.cwd, input.path);
802
+ if (existsSync2(abs) && !ctx.readFiles.has(abs)) {
803
+ return { output: `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
804
+ }
805
+ try {
806
+ await mkdir(dirname(abs), { recursive: true });
807
+ await writeFile2(abs, input.content);
808
+ } catch (error) {
809
+ return { output: `Gagal menulis ${input.path}: ${errorCode(error)}`, isError: true };
810
+ }
811
+ ctx.readFiles.add(abs);
812
+ const lines = input.content.split("\n").length;
813
+ return { output: `Menulis ${input.path} (${lines} baris).`, display: `${lines} baris ditulis` };
814
+ }
815
+ });
816
+
817
+ // src/tools/index.ts
818
+ var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fetchTool];
819
+ function toolDefinitions(tools) {
820
+ return tools.map((tool) => {
821
+ const { $schema: _ignored, ...parameters } = z8.toJSONSchema(tool.schema);
822
+ return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
823
+ });
824
+ }
825
+
826
+ // src/agent.ts
827
+ var estimateTokens = (text) => Math.ceil(text.length / 4);
828
+ var COMPACT_PROMPT = "Summarize the conversation so far for yourself so you can continue the work with no other context. Include: the user goals, decisions made, files touched and their current state, commands run and results, the task in progress and the next step. Be concise but complete.";
829
+ var Agent = class {
830
+ constructor(opts) {
831
+ this.opts = opts;
832
+ this.provider = opts.provider;
833
+ this.permissions = opts.permissions;
834
+ this.tools = opts.tools;
835
+ this.definitions = toolDefinitions(opts.tools);
836
+ this.messages = [...opts.history ?? []];
837
+ this.maxSteps = opts.maxSteps ?? 50;
838
+ this.contextWindow = opts.contextWindow ?? 128e3;
839
+ }
840
+ opts;
841
+ provider;
842
+ permissions;
843
+ messages;
844
+ totalUsage = { inputTokens: 0, outputTokens: 0 };
845
+ lastInputTokens = 0;
846
+ onEvent = () => {
847
+ };
848
+ askPermission = async () => "no";
849
+ tools;
850
+ definitions;
851
+ readFiles = /* @__PURE__ */ new Set();
852
+ maxSteps;
853
+ // ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
854
+ contextWindow;
855
+ push(message) {
856
+ this.messages.push(message);
857
+ this.opts.onMessage?.(message);
858
+ }
859
+ clear() {
860
+ this.messages = [];
861
+ this.readFiles.clear();
862
+ this.lastInputTokens = 0;
863
+ this.opts.onReset?.();
864
+ }
865
+ async compact(signal) {
866
+ const completion = await this.provider.chat({
867
+ messages: [{ role: "system", content: this.opts.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
868
+ // Some gateways reject tool_calls in history when no tools are declared.
869
+ tools: this.definitions,
870
+ signal
871
+ });
872
+ this.clear();
873
+ this.push({ role: "user", content: `Ringkasan percakapan sebelumnya:
874
+ ${completion.text}` });
875
+ this.push({ role: "assistant", content: "Oke, saya lanjutkan dari ringkasan ini." });
876
+ this.onEvent({ type: "compacted" });
877
+ }
878
+ async run(text, signal) {
879
+ try {
880
+ if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
881
+ this.push({ role: "user", content: text });
882
+ for (let step = 0; step < this.maxSteps; step++) {
883
+ if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
884
+ await this.compact(signal);
885
+ this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
886
+ }
887
+ const completion = await this.provider.chat({
888
+ messages: [{ role: "system", content: this.opts.systemPrompt }, ...this.messages],
889
+ tools: this.definitions,
890
+ signal,
891
+ onText: (delta) => this.onEvent({ type: "text", delta })
892
+ });
893
+ const usage = completion.usage ?? {
894
+ inputTokens: estimateTokens(this.opts.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
895
+ outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
896
+ };
897
+ this.totalUsage.inputTokens += usage.inputTokens;
898
+ this.totalUsage.outputTokens += usage.outputTokens;
899
+ this.lastInputTokens = usage.inputTokens;
900
+ this.onEvent({ type: "usage", ...this.totalUsage });
901
+ this.push({
902
+ role: "assistant",
903
+ content: completion.text || null,
904
+ ...completion.toolCalls.length ? {
905
+ tool_calls: completion.toolCalls.map((c) => ({
906
+ id: c.id,
907
+ type: "function",
908
+ function: { name: c.name, arguments: c.arguments }
909
+ }))
910
+ } : {}
911
+ });
912
+ if (!completion.toolCalls.length) {
913
+ this.onEvent({ type: "done" });
914
+ return;
915
+ }
916
+ for (let i = 0; i < completion.toolCalls.length; i++) {
917
+ const call = completion.toolCalls[i];
918
+ if (signal.aborted) {
919
+ for (const pending of completion.toolCalls.slice(i)) {
920
+ this.push({ role: "tool", tool_call_id: pending.id, content: "Dibatalkan oleh user." });
921
+ }
922
+ this.onEvent({ type: "aborted" });
923
+ return;
924
+ }
925
+ this.push({ role: "tool", tool_call_id: call.id, content: await this.runTool(call, signal) });
926
+ }
927
+ if (signal.aborted) {
928
+ this.onEvent({ type: "aborted" });
929
+ return;
930
+ }
931
+ }
932
+ this.onEvent({ type: "stepLimit" });
933
+ } catch (error) {
934
+ if (signal.aborted) {
935
+ this.onEvent({ type: "aborted" });
936
+ return;
937
+ }
938
+ const message = error.message;
939
+ const noTools = error.status === 400 && /tool/i.test(message);
940
+ this.onEvent({
941
+ type: "error",
942
+ message: noTools ? `${message}
943
+ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model.` : message
944
+ });
945
+ }
946
+ }
947
+ async runTool(call, signal) {
948
+ const fail = (output, target2 = "") => {
949
+ this.onEvent({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
950
+ this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
951
+ return output;
952
+ };
953
+ const tool = this.tools.find((t) => t.name === call.name);
954
+ if (!tool) return fail(`Alat "${call.name}" tidak ada. Alat yang tersedia: ${this.tools.map((t) => t.name).join(", ")}`);
955
+ let args;
956
+ try {
957
+ args = JSON.parse(call.arguments || "{}");
958
+ } catch {
959
+ return fail(`Argumen bukan JSON valid: ${call.arguments.slice(0, 200)}`);
960
+ }
961
+ const parsed = tool.schema.safeParse(args);
962
+ if (!parsed.success) {
963
+ return fail(`Argumen tidak valid: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
964
+ }
965
+ const input = parsed.data;
966
+ const target = tool.target(input);
967
+ const ctx = { cwd: this.opts.cwd, signal, readFiles: this.readFiles };
968
+ this.onEvent({ type: "toolStart", id: call.id, tool: tool.name, target });
969
+ const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
970
+ if (invalid) {
971
+ this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
972
+ return invalid;
973
+ }
974
+ const request = { tool: tool.name, kind: tool.kind, target };
975
+ let decision = this.permissions.check(request);
976
+ if (decision === "ask") {
977
+ const preview = await tool.preview?.(input, ctx).catch(() => void 0);
978
+ const answer = await this.askPermission({ ...request, preview, sessionRules: this.permissions.rulesFor(request) });
979
+ if (answer === "session") this.permissions.allowForSession(request);
980
+ decision = answer === "no" ? "deny" : "allow";
981
+ }
982
+ if (decision === "deny") {
983
+ const output = this.permissions.mode === "plan" ? "Ditolak: mode plan hanya boleh membaca dan mencari. Susun rencana untuk user tanpa mengubah apa pun." : "User menolak menjalankan alat ini. Tanyakan ke user apa yang diinginkan, atau coba cara lain.";
984
+ this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
985
+ return output;
986
+ }
987
+ let result;
988
+ try {
989
+ result = await tool.run(input, ctx);
990
+ } catch (error) {
991
+ result = { output: `Error: ${error.message}`, isError: true };
992
+ }
993
+ this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
994
+ return result.output;
995
+ }
996
+ };
997
+
998
+ // src/context.ts
999
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1000
+ import { dirname as dirname2, join as join2 } from "path";
1001
+ var FILES = ["AGENTS.md", "BCCLI.md"];
1002
+ function readIf(path) {
1003
+ try {
1004
+ return existsSync3(path) ? readFileSync2(path, "utf8").trim() : void 0;
1005
+ } catch {
1006
+ return void 0;
1007
+ }
1008
+ }
1009
+ function loadInstructions(cwd, home) {
1010
+ const dirs = [];
1011
+ for (let dir = cwd; ; dir = dirname2(dir)) {
1012
+ dirs.unshift(dir);
1013
+ if (dirname2(dir) === dir) break;
1014
+ }
1015
+ const parts = [];
1016
+ const global = readIf(join2(home, "BCCLI.md"));
1017
+ if (global) parts.push(`# ${join2(home, "BCCLI.md")}
1018
+ ${global}`);
1019
+ for (const dir of dirs) {
1020
+ for (const name of FILES) {
1021
+ const text = readIf(join2(dir, name));
1022
+ if (text) parts.push(`# ${join2(dir, name)}
1023
+ ${text}`);
1024
+ }
1025
+ }
1026
+ return parts.join("\n\n");
1027
+ }
1028
+ function isGitRepo(cwd) {
1029
+ for (let dir = cwd; ; dir = dirname2(dir)) {
1030
+ if (existsSync3(join2(dir, ".git"))) return true;
1031
+ if (dirname2(dir) === dir) return false;
1032
+ }
1033
+ }
1034
+ function buildSystemPrompt(opts) {
1035
+ const instructions = loadInstructions(opts.cwd, opts.home);
1036
+ 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
+
1038
+ How to work:
1039
+ - Understand before changing: read the relevant files and search the codebase first. Never guess file contents.
1040
+ - Always read a file before editing it. Make the smallest correct change; match the existing style.
1041
+ - Verify your work: run the tests, build or the command that proves the change works, and report the real result.
1042
+ - The user must approve edits and commands. If they decline, ask what they want instead of retrying.
1043
+ - Be concise. Reply in the user's language. No preamble; lead with the answer or the action.
1044
+ - Never expose secrets, never run destructive commands (rm -rf, force push, dropping data) unless the user explicitly asks.
1045
+
1046
+ Environment:
1047
+ - Working directory: ${opts.cwd}
1048
+ - Git repository: ${isGitRepo(opts.cwd) ? "yes" : "no"}
1049
+ - Platform: ${opts.platform ?? process.platform}
1050
+ - Date: ${opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
1051
+ - Model: ${opts.model}${instructions ? `
1052
+
1053
+ Project and user instructions (follow them):
1054
+
1055
+ ${instructions}` : ""}`;
1056
+ }
1057
+
1058
+ // src/setup.ts
1059
+ var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
1060
+ function createRuntime(opts) {
1061
+ const env = opts.env ?? process.env;
1062
+ const home = bccliHome(env);
1063
+ const config = loadConfig(opts.cwd, env);
1064
+ let modelRef = opts.args.model ?? config.model;
1065
+ const makeProvider = (ref) => {
1066
+ const resolved = resolveModel(config, ref, env);
1067
+ return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model });
1068
+ };
1069
+ const provider = makeProvider(modelRef);
1070
+ const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
1071
+ const rules = [...config.allow, ...opts.args.allowedTools.map((t) => TOOL_RULES[t] ?? t)];
1072
+ const permissions = new Permissions(mode, rules, opts.cwd);
1073
+ let session = Session.create(home, opts.cwd);
1074
+ let history = opts.history;
1075
+ if (!history && opts.args.continue) {
1076
+ const latest = Session.latest(home, opts.cwd);
1077
+ if (latest) {
1078
+ session = latest;
1079
+ history = latest.load();
1080
+ }
1081
+ }
1082
+ const agent = new Agent({
1083
+ provider,
1084
+ tools: ALL_TOOLS,
1085
+ permissions,
1086
+ systemPrompt: buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef }),
1087
+ cwd: opts.cwd,
1088
+ history,
1089
+ onMessage: (m) => session.append(m),
1090
+ onReset: () => session.reset()
1091
+ });
1092
+ return {
1093
+ cwd: opts.cwd,
1094
+ home,
1095
+ config,
1096
+ get modelRef() {
1097
+ return modelRef;
1098
+ },
1099
+ agent,
1100
+ get session() {
1101
+ return session;
1102
+ },
1103
+ resume(previous) {
1104
+ session = previous;
1105
+ agent.messages = previous.load();
1106
+ },
1107
+ setModel(ref) {
1108
+ agent.provider = makeProvider(ref);
1109
+ modelRef = ref;
1110
+ }
1111
+ };
1112
+ }
1113
+
1114
+ // src/version.ts
1115
+ var VERSION = true ? "0.2.0-beta.1" : "0.0.0-dev";
1116
+
1117
+ // src/cli.ts
1118
+ async function main() {
1119
+ const args = parseCliArgs(process.argv.slice(2));
1120
+ if (args.version) {
1121
+ console.log(VERSION);
1122
+ return 0;
1123
+ }
1124
+ if (args.help) {
1125
+ console.log(HELP_TEXT);
1126
+ return 0;
1127
+ }
1128
+ const cwd = process.cwd();
1129
+ if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
1130
+ if (args.command === "models") {
1131
+ const config = loadConfig(cwd);
1132
+ const resolved = resolveModel(config, args.model ?? config.model);
1133
+ const models = await createProvider(resolved).listModels();
1134
+ console.log(models.map((m) => `${resolved.providerId}/${m}`).join("\n"));
1135
+ return 0;
1136
+ }
1137
+ const rt = createRuntime({ cwd, args });
1138
+ try {
1139
+ pruneSessions(rt.home);
1140
+ } catch {
1141
+ }
1142
+ if (args.print) {
1143
+ if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
1144
+ return runPrint(rt, args.prompt);
1145
+ }
1146
+ if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
1147
+ const { startInteractive } = await import("./ui-U6OMOINK.js");
1148
+ return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
1149
+ }
1150
+ main().then(
1151
+ (code) => process.exit(code),
1152
+ (error) => {
1153
+ console.error(error instanceof ConfigError || error?.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? error.message : error);
1154
+ process.exit(1);
1155
+ }
1156
+ );