@botconnector/bccli 0.1.0-beta.4 → 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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/args-EVIFNGPQ.js +11 -0
- package/dist/chunk-5C6NQFSI.js +50 -0
- package/dist/chunk-5XN44Q55.js +70 -0
- package/dist/chunk-E4BWUMQ2.js +52 -0
- package/dist/chunk-MANM7HY6.js +248 -0
- package/dist/chunk-RDQ7HPWS.js +82 -0
- package/dist/chunk-SJH3YLW4.js +92 -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 +1246 -0
- 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/session-7UB77CEG.js +9 -0
- package/dist/ui-6PGC4T6N.js +833 -0
- package/package.json +33 -19
- package/bin/bccli.js +0 -173
- package/default-config/botconnector-cloud.jsonc +0 -21
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
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";
|
|
14
|
+
import {
|
|
15
|
+
Session,
|
|
16
|
+
pruneSessions
|
|
17
|
+
} from "./chunk-SJH3YLW4.js";
|
|
18
|
+
import {
|
|
19
|
+
ignorePatterns,
|
|
20
|
+
loadAgentDefs,
|
|
21
|
+
loadCommands,
|
|
22
|
+
loadSkills,
|
|
23
|
+
parseFrontmatter,
|
|
24
|
+
runPrint
|
|
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";
|
|
38
|
+
|
|
39
|
+
// src/tools/index.ts
|
|
40
|
+
import { z as z8 } from "zod";
|
|
41
|
+
|
|
42
|
+
// src/tools/bash.ts
|
|
43
|
+
import { spawn } from "child_process";
|
|
44
|
+
import { z } from "zod";
|
|
45
|
+
|
|
46
|
+
// src/tools/types.ts
|
|
47
|
+
function defineTool(tool) {
|
|
48
|
+
return tool;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/tools/bash.ts
|
|
52
|
+
var HALF = 15e3;
|
|
53
|
+
function killProcessTree(pid, platform = process.platform, spawnFn = spawn) {
|
|
54
|
+
if (platform === "win32") {
|
|
55
|
+
const killer = spawnFn("taskkill", ["/pid", String(pid), "/T", "/F"]);
|
|
56
|
+
killer.on("error", () => {
|
|
57
|
+
try {
|
|
58
|
+
process.kill(pid);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
process.kill(-pid, "SIGKILL");
|
|
66
|
+
} catch {
|
|
67
|
+
try {
|
|
68
|
+
process.kill(pid, "SIGKILL");
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
var active = /* @__PURE__ */ new Set();
|
|
74
|
+
var exitHookInstalled = false;
|
|
75
|
+
function killAllCommands() {
|
|
76
|
+
for (const kill of active) kill();
|
|
77
|
+
}
|
|
78
|
+
function runCommand(command, opts) {
|
|
79
|
+
return new Promise((resolve2) => {
|
|
80
|
+
const child = spawn(command, {
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
shell: true,
|
|
83
|
+
detached: process.platform !== "win32",
|
|
84
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
85
|
+
});
|
|
86
|
+
if (!exitHookInstalled) {
|
|
87
|
+
process.on("exit", killAllCommands);
|
|
88
|
+
exitHookInstalled = true;
|
|
89
|
+
}
|
|
90
|
+
child.stdout.setEncoding("utf8");
|
|
91
|
+
child.stderr.setEncoding("utf8");
|
|
92
|
+
let head = "";
|
|
93
|
+
let tail = "";
|
|
94
|
+
let dropped = 0;
|
|
95
|
+
let timedOut = false;
|
|
96
|
+
let aborted = false;
|
|
97
|
+
const onData = (data) => {
|
|
98
|
+
let s = data;
|
|
99
|
+
if (head.length < HALF) {
|
|
100
|
+
const take = s.slice(0, HALF - head.length);
|
|
101
|
+
head += take;
|
|
102
|
+
s = s.slice(take.length);
|
|
103
|
+
}
|
|
104
|
+
if (s) {
|
|
105
|
+
tail += s;
|
|
106
|
+
if (tail.length > HALF) {
|
|
107
|
+
dropped += tail.length - HALF;
|
|
108
|
+
tail = tail.slice(-HALF);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
child.stdout.on("data", onData);
|
|
113
|
+
child.stderr.on("data", onData);
|
|
114
|
+
const kill = () => {
|
|
115
|
+
if (child.pid !== void 0) killProcessTree(child.pid);
|
|
116
|
+
};
|
|
117
|
+
const killAsAborted = () => {
|
|
118
|
+
aborted = true;
|
|
119
|
+
kill();
|
|
120
|
+
};
|
|
121
|
+
active.add(killAsAborted);
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
timedOut = true;
|
|
124
|
+
kill();
|
|
125
|
+
}, opts.timeoutMs);
|
|
126
|
+
const onAbort = () => {
|
|
127
|
+
aborted = true;
|
|
128
|
+
kill();
|
|
129
|
+
};
|
|
130
|
+
if (opts.signal.aborted) onAbort();
|
|
131
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
132
|
+
const finish = (exitCode, extra = "") => {
|
|
133
|
+
active.delete(killAsAborted);
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
136
|
+
const output = dropped ? `${head}
|
|
137
|
+
\u2026 [${dropped} karakter dipotong] \u2026
|
|
138
|
+
${tail}` : head + tail;
|
|
139
|
+
resolve2({ output: output + extra, exitCode, timedOut, aborted });
|
|
140
|
+
};
|
|
141
|
+
child.on("error", (error) => finish(null, error.message));
|
|
142
|
+
child.on("close", (code) => finish(code));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
var bashTool = defineTool({
|
|
146
|
+
name: "bash",
|
|
147
|
+
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.",
|
|
148
|
+
schema: z.object({
|
|
149
|
+
command: z.string().describe("The shell command to run"),
|
|
150
|
+
timeout_ms: z.number().int().min(1e3).max(6e5).optional().describe("Timeout in milliseconds")
|
|
151
|
+
}),
|
|
152
|
+
kind: "bash",
|
|
153
|
+
target: (input) => input.command,
|
|
154
|
+
async run(input, ctx) {
|
|
155
|
+
const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
|
|
156
|
+
const status = r.timedOut ? `[dihentikan: timeout ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} detik]` : r.aborted ? "[dibatalkan user]" : `[exit code ${r.exitCode}]`;
|
|
157
|
+
return { output: `${r.output.trimEnd()}
|
|
158
|
+
${status}`.trimStart(), isError: r.exitCode !== 0 };
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// src/tools/edit.ts
|
|
163
|
+
import { readFile, writeFile } from "fs/promises";
|
|
164
|
+
import { z as z2 } from "zod";
|
|
165
|
+
|
|
166
|
+
// src/diff.ts
|
|
167
|
+
import { structuredPatch } from "diff";
|
|
168
|
+
function diffLines(oldText, newText) {
|
|
169
|
+
const patch = structuredPatch("a", "b", oldText, newText, "", "", { context: 2 });
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const hunk of patch.hunks) {
|
|
172
|
+
let oldLine = hunk.oldStart;
|
|
173
|
+
let newLine = hunk.newStart;
|
|
174
|
+
for (const raw of hunk.lines) {
|
|
175
|
+
const text = raw.slice(1);
|
|
176
|
+
if (raw[0] === "-") out.push({ kind: "del", line: oldLine++, text });
|
|
177
|
+
else if (raw[0] === "+") out.push({ kind: "add", line: newLine++, text });
|
|
178
|
+
else if (raw[0] === " ") {
|
|
179
|
+
out.push({ kind: "ctx", line: newLine, text });
|
|
180
|
+
oldLine++;
|
|
181
|
+
newLine++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
function formatDiff(lines) {
|
|
188
|
+
const mark = { add: "+", del: "-", ctx: " " };
|
|
189
|
+
return lines.map((l) => `${String(l.line).padStart(5)} ${mark[l.kind]} ${l.text}`).join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/tools/paths.ts
|
|
193
|
+
import { isAbsolute, resolve } from "path";
|
|
194
|
+
function resolvePath(cwd, path) {
|
|
195
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
196
|
+
}
|
|
197
|
+
function errorCode(error) {
|
|
198
|
+
return error.code ?? error.message;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/tools/edit.ts
|
|
202
|
+
var schema = z2.object({
|
|
203
|
+
path: z2.string().describe("File path, relative to the project or absolute"),
|
|
204
|
+
old_string: z2.string().describe("Exact text to replace; must be unique unless replace_all"),
|
|
205
|
+
new_string: z2.string().describe("Replacement text"),
|
|
206
|
+
replace_all: z2.boolean().optional().describe("Replace every occurrence")
|
|
207
|
+
});
|
|
208
|
+
function countOccurrences(haystack, needle) {
|
|
209
|
+
let count = 0;
|
|
210
|
+
for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
|
|
211
|
+
return count;
|
|
212
|
+
}
|
|
213
|
+
function applyEdit(content, input) {
|
|
214
|
+
if (input.old_string === input.new_string) {
|
|
215
|
+
return { output: "old_string dan new_string sama; tidak ada perubahan.", isError: true };
|
|
216
|
+
}
|
|
217
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
218
|
+
const toEol = (s) => s.replace(/\r?\n/g, eol);
|
|
219
|
+
const attempts = [[input.old_string, input.new_string]];
|
|
220
|
+
if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
|
|
221
|
+
for (const [oldText, newText] of attempts) {
|
|
222
|
+
const count = oldText ? countOccurrences(content, oldText) : 0;
|
|
223
|
+
if (count === 0) continue;
|
|
224
|
+
if (count > 1 && !input.replace_all) {
|
|
225
|
+
return {
|
|
226
|
+
output: `old_string muncul ${count} kali di ${input.path}. Tambah konteks agar unik, atau pakai replace_all.`,
|
|
227
|
+
isError: true
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
|
|
231
|
+
}
|
|
232
|
+
return { output: `old_string tidak ditemukan di ${input.path}. Baca ulang file-nya.`, isError: true };
|
|
233
|
+
}
|
|
234
|
+
var editTool = defineTool({
|
|
235
|
+
name: "edit",
|
|
236
|
+
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.",
|
|
237
|
+
schema,
|
|
238
|
+
kind: "edit",
|
|
239
|
+
target: (input) => input.path,
|
|
240
|
+
async validate(input, ctx) {
|
|
241
|
+
const abs = resolvePath(ctx.cwd, input.path);
|
|
242
|
+
if (!ctx.readFiles.has(abs)) return `Baca ${input.path} dulu dengan read sebelum mengedit.`;
|
|
243
|
+
let content;
|
|
244
|
+
try {
|
|
245
|
+
content = await readFile(abs, "utf8");
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return `Tidak bisa membaca ${input.path}: ${errorCode(error)}`;
|
|
248
|
+
}
|
|
249
|
+
const next = applyEdit(content, input);
|
|
250
|
+
return typeof next === "string" ? void 0 : next.output;
|
|
251
|
+
},
|
|
252
|
+
async preview(input, ctx) {
|
|
253
|
+
const abs = resolvePath(ctx.cwd, input.path);
|
|
254
|
+
const content = await readFile(abs, "utf8");
|
|
255
|
+
const next = applyEdit(content, input);
|
|
256
|
+
return typeof next === "string" ? formatDiff(diffLines(content, next)) : void 0;
|
|
257
|
+
},
|
|
258
|
+
async run(input, ctx) {
|
|
259
|
+
const abs = resolvePath(ctx.cwd, input.path);
|
|
260
|
+
if (!ctx.readFiles.has(abs)) {
|
|
261
|
+
return { output: `Baca ${input.path} dulu dengan read sebelum mengedit.`, isError: true };
|
|
262
|
+
}
|
|
263
|
+
let content;
|
|
264
|
+
try {
|
|
265
|
+
content = await readFile(abs, "utf8");
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
|
|
268
|
+
}
|
|
269
|
+
const next = applyEdit(content, input);
|
|
270
|
+
if (typeof next !== "string") return next;
|
|
271
|
+
await writeFile(abs, next);
|
|
272
|
+
return { output: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/tools/fetch.ts
|
|
277
|
+
import { z as z3 } from "zod";
|
|
278
|
+
var MAX_CHARS = 5e4;
|
|
279
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
280
|
+
async function readCapped(res) {
|
|
281
|
+
const reader = res.body?.getReader();
|
|
282
|
+
if (!reader) return { text: "", truncated: false };
|
|
283
|
+
const chunks = [];
|
|
284
|
+
let size = 0;
|
|
285
|
+
for (; ; ) {
|
|
286
|
+
const { done, value } = await reader.read();
|
|
287
|
+
if (done) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
|
|
288
|
+
size += value.length;
|
|
289
|
+
if (size > MAX_BYTES) {
|
|
290
|
+
chunks.push(value.subarray(0, value.length - (size - MAX_BYTES)));
|
|
291
|
+
await reader.cancel();
|
|
292
|
+
return { text: Buffer.concat(chunks).toString("utf8"), truncated: true };
|
|
293
|
+
}
|
|
294
|
+
chunks.push(value);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function htmlToText(html) {
|
|
298
|
+
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
299
|
+
}
|
|
300
|
+
var fetchTool = defineTool({
|
|
301
|
+
name: "fetch",
|
|
302
|
+
description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
|
|
303
|
+
schema: z3.object({ url: z3.string().describe("Absolute http(s) URL") }),
|
|
304
|
+
kind: "fetch",
|
|
305
|
+
target: (input) => input.url,
|
|
306
|
+
async run(input, ctx) {
|
|
307
|
+
if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
|
|
308
|
+
let url = new URL(input.url);
|
|
309
|
+
let res;
|
|
310
|
+
try {
|
|
311
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
312
|
+
res = await fetch(url, { signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
|
|
313
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
314
|
+
if (!location) break;
|
|
315
|
+
const next = new URL(location, url);
|
|
316
|
+
if (next.host !== url.host) {
|
|
317
|
+
await res.body?.cancel();
|
|
318
|
+
return {
|
|
319
|
+
output: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).`,
|
|
320
|
+
isError: true
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
await res.body?.cancel();
|
|
324
|
+
url = next;
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
|
|
328
|
+
}
|
|
329
|
+
if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
|
|
330
|
+
const { text: body, truncated } = await readCapped(res);
|
|
331
|
+
if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${body.slice(0, 500)}`, isError: true };
|
|
332
|
+
const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
|
|
333
|
+
const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
|
|
334
|
+
\u2026 [dipotong]` : text;
|
|
335
|
+
const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
336
|
+
return { output: clipped, display };
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// src/tools/glob.ts
|
|
341
|
+
import fg from "fast-glob";
|
|
342
|
+
import { z as z4 } from "zod";
|
|
343
|
+
var globTool = defineTool({
|
|
344
|
+
name: "glob",
|
|
345
|
+
description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
|
|
346
|
+
schema: z4.object({
|
|
347
|
+
pattern: z4.string().describe("Glob pattern"),
|
|
348
|
+
path: z4.string().optional().describe("Base directory, default project root")
|
|
349
|
+
}),
|
|
350
|
+
kind: "read",
|
|
351
|
+
target: (input) => input.pattern,
|
|
352
|
+
async run(input, ctx) {
|
|
353
|
+
const root = resolvePath(ctx.cwd, input.path ?? ".");
|
|
354
|
+
const files = (await fg(input.pattern, { cwd: root, ignore: ignorePatterns(root), onlyFiles: true, dot: true })).sort();
|
|
355
|
+
if (!files.length) return { output: "Tidak ada file yang cocok." };
|
|
356
|
+
const shown = files.slice(0, 500);
|
|
357
|
+
const more = files.length > shown.length ? `
|
|
358
|
+
\u2026 ${files.length - shown.length} file lagi` : "";
|
|
359
|
+
return { output: shown.join("\n") + more, display: `${files.length} file` };
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// src/tools/grep.ts
|
|
364
|
+
import { execFile } from "child_process";
|
|
365
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
366
|
+
import fg2 from "fast-glob";
|
|
367
|
+
import { z as z5 } from "zod";
|
|
368
|
+
var MAX_MATCHES = 200;
|
|
369
|
+
async function grepJs(pattern, opts) {
|
|
370
|
+
const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
|
|
371
|
+
const files = await fg2(opts.glob ?? "**/*", { cwd: opts.root, ignore: ignorePatterns(opts.root), onlyFiles: true, dot: true });
|
|
372
|
+
const out = [];
|
|
373
|
+
for (const file of files.sort()) {
|
|
374
|
+
let text;
|
|
375
|
+
try {
|
|
376
|
+
const buffer = await readFile2(resolvePath(opts.root, file));
|
|
377
|
+
if (buffer.subarray(0, 8e3).includes(0)) continue;
|
|
378
|
+
text = buffer.toString("utf8");
|
|
379
|
+
} catch {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const lines = text.split(/\r?\n/);
|
|
383
|
+
for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
|
|
384
|
+
if (regex.test(lines[i])) out.push(`${file}:${i + 1}:${lines[i].slice(0, 300)}`);
|
|
385
|
+
}
|
|
386
|
+
if (out.length >= MAX_MATCHES) break;
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
function grepRg(pattern, opts) {
|
|
391
|
+
const args = ["--line-number", "--no-heading", "--color", "never", "--max-columns", "300", "-e", pattern];
|
|
392
|
+
if (opts.ignoreCase) args.push("-i");
|
|
393
|
+
if (opts.glob) args.push("-g", opts.glob);
|
|
394
|
+
args.push(".");
|
|
395
|
+
return new Promise((resolve2) => {
|
|
396
|
+
execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
|
|
397
|
+
const code = error?.code;
|
|
398
|
+
if (code === "ENOENT") return resolve2(void 0);
|
|
399
|
+
if (error && error.code !== 1) return resolve2(void 0);
|
|
400
|
+
resolve2(stdout.split("\n").filter(Boolean).map((l) => l.replace(/^\.\//, "")).slice(0, MAX_MATCHES));
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
var grepTool = defineTool({
|
|
405
|
+
name: "grep",
|
|
406
|
+
description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
|
|
407
|
+
schema: z5.object({
|
|
408
|
+
pattern: z5.string().describe("Regular expression"),
|
|
409
|
+
path: z5.string().optional().describe("Directory to search, default project root"),
|
|
410
|
+
glob: z5.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
|
|
411
|
+
ignore_case: z5.boolean().optional()
|
|
412
|
+
}),
|
|
413
|
+
kind: "read",
|
|
414
|
+
target: (input) => input.pattern,
|
|
415
|
+
async run(input, ctx) {
|
|
416
|
+
const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case };
|
|
417
|
+
try {
|
|
418
|
+
new RegExp(input.pattern);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return { output: `Regex tidak valid: ${error.message}`, isError: true };
|
|
421
|
+
}
|
|
422
|
+
const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
|
|
423
|
+
if (!matches.length) return { output: "Tidak ada yang cocok." };
|
|
424
|
+
return { output: matches.join("\n"), display: `${matches.length} hasil` };
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// src/tools/read.ts
|
|
429
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
430
|
+
import { z as z6 } from "zod";
|
|
431
|
+
var MAX_LINE = 2e3;
|
|
432
|
+
var readTool = defineTool({
|
|
433
|
+
name: "read",
|
|
434
|
+
description: "Read a text file. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it.",
|
|
435
|
+
schema: z6.object({
|
|
436
|
+
path: z6.string().describe("File path, relative to the project or absolute"),
|
|
437
|
+
offset: z6.number().int().min(1).optional().describe("First line to read, 1-based"),
|
|
438
|
+
limit: z6.number().int().min(1).optional().describe("Number of lines, default 2000")
|
|
439
|
+
}),
|
|
440
|
+
kind: "read",
|
|
441
|
+
target: (input) => input.path,
|
|
442
|
+
async run(input, ctx) {
|
|
443
|
+
const abs = resolvePath(ctx.cwd, input.path);
|
|
444
|
+
let buffer;
|
|
445
|
+
try {
|
|
446
|
+
buffer = await readFile3(abs);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
|
|
449
|
+
}
|
|
450
|
+
if (buffer.subarray(0, 8e3).includes(0)) {
|
|
451
|
+
return { output: `${input.path} adalah file biner, tidak dibaca.`, isError: true };
|
|
452
|
+
}
|
|
453
|
+
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
454
|
+
const start = (input.offset ?? 1) - 1;
|
|
455
|
+
const limit = input.limit ?? 2e3;
|
|
456
|
+
const slice = lines.slice(start, start + limit);
|
|
457
|
+
const body = slice.map((line, i) => {
|
|
458
|
+
const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [baris dipotong]` : line;
|
|
459
|
+
return `${String(start + i + 1).padStart(6)} ${text}`;
|
|
460
|
+
}).join("\n");
|
|
461
|
+
const remaining = lines.length - start - slice.length;
|
|
462
|
+
ctx.readFiles.add(abs);
|
|
463
|
+
return {
|
|
464
|
+
output: remaining > 0 ? `${body}
|
|
465
|
+
\u2026 ${remaining} baris lagi (pakai offset)` : body,
|
|
466
|
+
display: `${slice.length} baris`
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// src/tools/write.ts
|
|
472
|
+
import { existsSync } from "fs";
|
|
473
|
+
import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
474
|
+
import { dirname } from "path";
|
|
475
|
+
import { z as z7 } from "zod";
|
|
476
|
+
var writeTool = defineTool({
|
|
477
|
+
name: "write",
|
|
478
|
+
description: "Create or overwrite a file with the given content. Read an existing file first. Prefer edit for changes.",
|
|
479
|
+
schema: z7.object({
|
|
480
|
+
path: z7.string().describe("File path, relative to the project or absolute"),
|
|
481
|
+
content: z7.string().describe("Full new file content")
|
|
482
|
+
}),
|
|
483
|
+
kind: "edit",
|
|
484
|
+
target: (input) => input.path,
|
|
485
|
+
async validate(input, ctx) {
|
|
486
|
+
const abs = resolvePath(ctx.cwd, input.path);
|
|
487
|
+
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.` : void 0;
|
|
488
|
+
},
|
|
489
|
+
async preview(input, ctx) {
|
|
490
|
+
const abs = resolvePath(ctx.cwd, input.path);
|
|
491
|
+
const old = existsSync(abs) ? await readFile4(abs, "utf8") : "";
|
|
492
|
+
return formatDiff(diffLines(old, input.content));
|
|
493
|
+
},
|
|
494
|
+
async run(input, ctx) {
|
|
495
|
+
const abs = resolvePath(ctx.cwd, input.path);
|
|
496
|
+
if (existsSync(abs) && !ctx.readFiles.has(abs)) {
|
|
497
|
+
return { output: `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
501
|
+
await writeFile2(abs, input.content);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return { output: `Gagal menulis ${input.path}: ${errorCode(error)}`, isError: true };
|
|
504
|
+
}
|
|
505
|
+
ctx.readFiles.add(abs);
|
|
506
|
+
const lines = input.content.split("\n").length;
|
|
507
|
+
return { output: `Menulis ${input.path} (${lines} baris).`, display: `${lines} baris ditulis` };
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// src/tools/index.ts
|
|
512
|
+
var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fetchTool];
|
|
513
|
+
function toolDefinitions(tools) {
|
|
514
|
+
return tools.map((tool) => {
|
|
515
|
+
const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z8.toJSONSchema(tool.schema);
|
|
516
|
+
return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/agent.ts
|
|
521
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
522
|
+
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.";
|
|
523
|
+
var Agent = class {
|
|
524
|
+
constructor(opts) {
|
|
525
|
+
this.opts = opts;
|
|
526
|
+
this.provider = opts.provider;
|
|
527
|
+
this.permissions = opts.permissions;
|
|
528
|
+
this.tools = opts.tools;
|
|
529
|
+
this.definitions = toolDefinitions(opts.tools);
|
|
530
|
+
this.turnTools = this.tools;
|
|
531
|
+
this.turnDefinitions = this.definitions;
|
|
532
|
+
this.messages = [...opts.history ?? []];
|
|
533
|
+
this.maxSteps = opts.maxSteps ?? 50;
|
|
534
|
+
this.contextWindow = opts.contextWindow ?? 128e3;
|
|
535
|
+
}
|
|
536
|
+
opts;
|
|
537
|
+
provider;
|
|
538
|
+
permissions;
|
|
539
|
+
messages;
|
|
540
|
+
totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
541
|
+
lastInputTokens = 0;
|
|
542
|
+
onEvent = () => {
|
|
543
|
+
};
|
|
544
|
+
askPermission = async () => "no";
|
|
545
|
+
tools;
|
|
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;
|
|
550
|
+
readFiles = /* @__PURE__ */ new Set();
|
|
551
|
+
maxSteps;
|
|
552
|
+
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
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
|
+
}
|
|
568
|
+
push(message) {
|
|
569
|
+
this.messages.push(message);
|
|
570
|
+
this.opts.onMessage?.(message);
|
|
571
|
+
}
|
|
572
|
+
clear() {
|
|
573
|
+
this.messages = [];
|
|
574
|
+
this.readFiles.clear();
|
|
575
|
+
this.lastInputTokens = 0;
|
|
576
|
+
this.opts.onReset?.();
|
|
577
|
+
}
|
|
578
|
+
async compact(signal) {
|
|
579
|
+
const completion = await this.provider.chat({
|
|
580
|
+
messages: [{ role: "system", content: this.opts.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
|
|
581
|
+
// Some gateways reject tool_calls in history when no tools are declared.
|
|
582
|
+
tools: this.definitions,
|
|
583
|
+
signal
|
|
584
|
+
});
|
|
585
|
+
this.clear();
|
|
586
|
+
this.push({ role: "user", content: `Ringkasan percakapan sebelumnya:
|
|
587
|
+
${completion.text}` });
|
|
588
|
+
this.push({ role: "assistant", content: "Oke, saya lanjutkan dari ringkasan ini." });
|
|
589
|
+
this.onEvent({ type: "compacted" });
|
|
590
|
+
}
|
|
591
|
+
async run(text, signal) {
|
|
592
|
+
this.turnTools = this.tools;
|
|
593
|
+
this.turnDefinitions = this.definitions;
|
|
594
|
+
try {
|
|
595
|
+
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
596
|
+
this.push({ role: "user", content: text });
|
|
597
|
+
for (let step = 0; step < this.maxSteps; step++) {
|
|
598
|
+
if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
|
|
599
|
+
await this.compact(signal);
|
|
600
|
+
this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
|
|
601
|
+
}
|
|
602
|
+
const completion = await this.provider.chat({
|
|
603
|
+
messages: [{ role: "system", content: this.opts.systemPrompt }, ...this.messages],
|
|
604
|
+
tools: this.turnDefinitions,
|
|
605
|
+
signal,
|
|
606
|
+
onText: (delta) => this.onEvent({ type: "text", delta })
|
|
607
|
+
});
|
|
608
|
+
const usage = completion.usage ?? {
|
|
609
|
+
inputTokens: estimateTokens(this.opts.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
|
|
610
|
+
outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
|
|
611
|
+
};
|
|
612
|
+
this.totalUsage.inputTokens += usage.inputTokens;
|
|
613
|
+
this.totalUsage.outputTokens += usage.outputTokens;
|
|
614
|
+
this.lastInputTokens = usage.inputTokens;
|
|
615
|
+
this.onEvent({ type: "usage", ...this.totalUsage });
|
|
616
|
+
this.push({
|
|
617
|
+
role: "assistant",
|
|
618
|
+
content: completion.text || null,
|
|
619
|
+
...completion.toolCalls.length ? {
|
|
620
|
+
tool_calls: completion.toolCalls.map((c) => ({
|
|
621
|
+
id: c.id,
|
|
622
|
+
type: "function",
|
|
623
|
+
function: { name: c.name, arguments: c.arguments }
|
|
624
|
+
}))
|
|
625
|
+
} : {}
|
|
626
|
+
});
|
|
627
|
+
if (!completion.toolCalls.length) {
|
|
628
|
+
this.onEvent({ type: "done" });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const calls = completion.toolCalls;
|
|
632
|
+
let i = 0;
|
|
633
|
+
while (i < calls.length) {
|
|
634
|
+
if (signal.aborted) {
|
|
635
|
+
for (const pending of calls.slice(i)) this.push({ role: "tool", tool_call_id: pending.id, content: "Dibatalkan oleh user." });
|
|
636
|
+
this.onEvent({ type: "aborted" });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
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;
|
|
647
|
+
}
|
|
648
|
+
if (signal.aborted) {
|
|
649
|
+
this.onEvent({ type: "aborted" });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
this.onEvent({ type: "stepLimit" });
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (signal.aborted) {
|
|
656
|
+
this.onEvent({ type: "aborted" });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const message = error.message;
|
|
660
|
+
const noTools = error.status === 400 && /tool/i.test(message);
|
|
661
|
+
this.onEvent({
|
|
662
|
+
type: "error",
|
|
663
|
+
message: noTools ? `${message}
|
|
664
|
+
Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model.` : message
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async runTool(call, signal) {
|
|
669
|
+
const fail = (output, target2 = "") => {
|
|
670
|
+
this.onEvent({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
|
|
671
|
+
this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
|
|
672
|
+
return output;
|
|
673
|
+
};
|
|
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(", ")}`);
|
|
676
|
+
let args;
|
|
677
|
+
try {
|
|
678
|
+
args = JSON.parse(call.arguments || "{}");
|
|
679
|
+
} catch {
|
|
680
|
+
return fail(`Argumen bukan JSON valid: ${call.arguments.slice(0, 200)}`);
|
|
681
|
+
}
|
|
682
|
+
const parsed = tool.schema.safeParse(args);
|
|
683
|
+
if (!parsed.success) {
|
|
684
|
+
return fail(`Argumen tidak valid: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
685
|
+
}
|
|
686
|
+
const input = parsed.data;
|
|
687
|
+
const target = tool.target(input);
|
|
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
|
+
};
|
|
701
|
+
this.onEvent({ type: "toolStart", id: call.id, tool: tool.name, target });
|
|
702
|
+
const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
|
|
703
|
+
if (invalid) {
|
|
704
|
+
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
|
|
705
|
+
return invalid;
|
|
706
|
+
}
|
|
707
|
+
const request = { tool: tool.name, kind: tool.kind, target };
|
|
708
|
+
let decision = this.permissions.check(request);
|
|
709
|
+
if (decision === "ask") {
|
|
710
|
+
const preview = await tool.preview?.(input, ctx).catch(() => void 0);
|
|
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
|
+
});
|
|
717
|
+
if (answer === "session") this.permissions.allowForSession(request);
|
|
718
|
+
decision = answer === "no" ? "deny" : "allow";
|
|
719
|
+
}
|
|
720
|
+
if (decision === "deny") {
|
|
721
|
+
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.";
|
|
722
|
+
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
|
|
723
|
+
return output;
|
|
724
|
+
}
|
|
725
|
+
let result;
|
|
726
|
+
try {
|
|
727
|
+
result = await tool.run(input, ctx);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
result = { output: `Error: ${error.message}`, isError: true };
|
|
730
|
+
}
|
|
731
|
+
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
|
|
732
|
+
return result.output;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
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
|
+
|
|
865
|
+
// src/context.ts
|
|
866
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
867
|
+
import { dirname as dirname2, join } from "path";
|
|
868
|
+
var FILES = ["AGENTS.md", "BCCLI.md"];
|
|
869
|
+
function readIf(path) {
|
|
870
|
+
try {
|
|
871
|
+
return existsSync2(path) ? readFileSync(path, "utf8").trim() : void 0;
|
|
872
|
+
} catch {
|
|
873
|
+
return void 0;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function loadInstructions(cwd, home) {
|
|
877
|
+
const dirs = [];
|
|
878
|
+
for (let dir = cwd; ; dir = dirname2(dir)) {
|
|
879
|
+
dirs.unshift(dir);
|
|
880
|
+
if (dirname2(dir) === dir) break;
|
|
881
|
+
}
|
|
882
|
+
const parts = [];
|
|
883
|
+
const global = readIf(join(home, "BCCLI.md"));
|
|
884
|
+
if (global) parts.push(`# ${join(home, "BCCLI.md")}
|
|
885
|
+
${global}`);
|
|
886
|
+
for (const dir of dirs) {
|
|
887
|
+
for (const name of FILES) {
|
|
888
|
+
const text = readIf(join(dir, name));
|
|
889
|
+
if (text) parts.push(`# ${join(dir, name)}
|
|
890
|
+
${text}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return parts.join("\n\n");
|
|
894
|
+
}
|
|
895
|
+
function isGitRepo(cwd) {
|
|
896
|
+
for (let dir = cwd; ; dir = dirname2(dir)) {
|
|
897
|
+
if (existsSync2(join(dir, ".git"))) return true;
|
|
898
|
+
if (dirname2(dir) === dir) return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function buildSystemPrompt(opts) {
|
|
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")}` : "";
|
|
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.
|
|
909
|
+
|
|
910
|
+
How to work:
|
|
911
|
+
- Understand before changing: read the relevant files and search the codebase first. Never guess file contents.
|
|
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.
|
|
915
|
+
- Verify your work: run the tests, build or the command that proves the change works, and report the real result.
|
|
916
|
+
- The user must approve edits and commands. If they decline, ask what they want instead of retrying.
|
|
917
|
+
- Be concise. Reply in the user's language. No preamble; lead with the answer or the action.
|
|
918
|
+
- Never expose secrets, never run destructive commands (rm -rf, force push, dropping data) unless the user explicitly asks.
|
|
919
|
+
|
|
920
|
+
Environment:
|
|
921
|
+
- Working directory: ${opts.cwd}
|
|
922
|
+
- Git repository: ${isGitRepo(opts.cwd) ? "yes" : "no"}
|
|
923
|
+
- Platform: ${opts.platform ?? process.platform}
|
|
924
|
+
- Date: ${opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
925
|
+
- Model: ${opts.model}${skillText}${instructions ? `
|
|
926
|
+
|
|
927
|
+
Project and user instructions (follow them):
|
|
928
|
+
|
|
929
|
+
${instructions}` : ""}`;
|
|
930
|
+
}
|
|
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
|
+
|
|
1081
|
+
// src/setup.ts
|
|
1082
|
+
var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
|
|
1083
|
+
function createRuntime(opts) {
|
|
1084
|
+
const env = opts.env ?? process.env;
|
|
1085
|
+
const home = bccliHome(env);
|
|
1086
|
+
let config = loadConfig(opts.cwd, env);
|
|
1087
|
+
let modelRef = opts.args.model ?? config.model;
|
|
1088
|
+
const makeProvider = (ref) => {
|
|
1089
|
+
const resolved = resolveModel(config, ref, env);
|
|
1090
|
+
return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model });
|
|
1091
|
+
};
|
|
1092
|
+
const provider = makeProvider(modelRef);
|
|
1093
|
+
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
1094
|
+
const rules = [...config.allow, ...opts.args.allowedTools.map((t) => TOOL_RULES[t] ?? t)];
|
|
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" };
|
|
1102
|
+
let session = Session.create(home, opts.cwd);
|
|
1103
|
+
let history = opts.history;
|
|
1104
|
+
if (!history && opts.args.continue) {
|
|
1105
|
+
const latest = Session.latest(home, opts.cwd);
|
|
1106
|
+
if (latest) {
|
|
1107
|
+
session = latest;
|
|
1108
|
+
history = latest.load();
|
|
1109
|
+
}
|
|
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
|
+
});
|
|
1123
|
+
const agent = new Agent({
|
|
1124
|
+
provider,
|
|
1125
|
+
tools: [...baseTools, task],
|
|
1126
|
+
permissions,
|
|
1127
|
+
systemPrompt,
|
|
1128
|
+
cwd: opts.cwd,
|
|
1129
|
+
history,
|
|
1130
|
+
onMessage: (m) => session.append(m),
|
|
1131
|
+
onReset: () => session.reset()
|
|
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
|
+
});
|
|
1140
|
+
return {
|
|
1141
|
+
cwd: opts.cwd,
|
|
1142
|
+
home,
|
|
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
|
+
},
|
|
1166
|
+
get modelRef() {
|
|
1167
|
+
return modelRef;
|
|
1168
|
+
},
|
|
1169
|
+
agent,
|
|
1170
|
+
get session() {
|
|
1171
|
+
return session;
|
|
1172
|
+
},
|
|
1173
|
+
resume(previous) {
|
|
1174
|
+
session = previous;
|
|
1175
|
+
agent.messages = previous.load();
|
|
1176
|
+
},
|
|
1177
|
+
setModel(ref) {
|
|
1178
|
+
agent.provider = makeProvider(ref);
|
|
1179
|
+
modelRef = ref;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/version.ts
|
|
1185
|
+
var VERSION = true ? "0.3.0-beta.1" : "0.0.0-dev";
|
|
1186
|
+
|
|
1187
|
+
// src/cli.ts
|
|
1188
|
+
async function main() {
|
|
1189
|
+
const args = parseCliArgs(process.argv.slice(2));
|
|
1190
|
+
if (args.version) {
|
|
1191
|
+
console.log(VERSION);
|
|
1192
|
+
return 0;
|
|
1193
|
+
}
|
|
1194
|
+
if (args.help) {
|
|
1195
|
+
console.log(HELP_TEXT);
|
|
1196
|
+
return 0;
|
|
1197
|
+
}
|
|
1198
|
+
const cwd = process.cwd();
|
|
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
|
+
}
|
|
1210
|
+
if (args.command === "models") {
|
|
1211
|
+
const config = loadConfig(cwd);
|
|
1212
|
+
const resolved = resolveModel(config, args.model ?? config.model);
|
|
1213
|
+
const models = await createProvider(resolved).listModels();
|
|
1214
|
+
console.log(models.map((m) => `${resolved.providerId}/${m}`).join("\n"));
|
|
1215
|
+
return 0;
|
|
1216
|
+
}
|
|
1217
|
+
const rt = createRuntime({ cwd, args });
|
|
1218
|
+
try {
|
|
1219
|
+
pruneSessions(rt.home);
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
if (args.print) {
|
|
1223
|
+
if (!args.prompt) throw new ConfigError('Mode -p butuh tugas, contoh: bccli -p "jelaskan repo ini"');
|
|
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
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
|
|
1237
|
+
const { startInteractive } = await import("./ui-6PGC4T6N.js");
|
|
1238
|
+
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1239
|
+
}
|
|
1240
|
+
main().then(
|
|
1241
|
+
(code) => process.exit(code),
|
|
1242
|
+
(error) => {
|
|
1243
|
+
console.error(error instanceof ConfigError || error?.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? error.message : error);
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
);
|