@botconnector/bccli 0.4.0-beta.5 → 0.4.0-beta.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -2
- package/dist/acp-NWP4FFTG.js +182 -0
- package/dist/args-2R45YZ42.js +13 -0
- package/dist/{chunk-U7DLMVPZ.js → chunk-2QYHSESB.js} +2 -1
- package/dist/{chunk-UESQNU4A.js → chunk-4WVIPZFV.js} +14 -9
- package/dist/chunk-7F3IWBPQ.js +191 -0
- package/dist/chunk-DP7EBW53.js +341 -0
- package/dist/{chunk-SJH3YLW4.js → chunk-F6ACN47V.js} +60 -6
- package/dist/chunk-FTFVW7XD.js +235 -0
- package/dist/{chunk-ELML4KVQ.js → chunk-GAFJEQTJ.js} +32 -2
- package/dist/{chunk-NGLKANMD.js → chunk-H73FO5YK.js} +24 -2
- package/dist/chunk-KD2LMIS4.js +145 -0
- package/dist/chunk-KIZC5XV4.js +2141 -0
- package/dist/chunk-N7KP6RNP.js +76 -0
- package/dist/chunk-NDEJUL44.js +361 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-QXOV4DUZ.js +56 -0
- package/dist/chunk-QZKLZELZ.js +64 -0
- package/dist/chunk-R4BJBTWC.js +940 -0
- package/dist/chunk-SNKVS26H.js +63 -0
- package/dist/chunk-TQWCM5RO.js +231 -0
- package/dist/chunk-VU5HNQHX.js +8 -0
- package/dist/chunk-XLC6T4SX.js +500 -0
- package/dist/chunk-YIYX3FFS.js +733 -0
- package/dist/cli.js +157 -1281
- package/dist/completion-OV3MW2N4.js +125 -0
- package/dist/{config-NPSYFAFD.js → config-AZZFOXKV.js} +5 -2
- package/dist/config-H5A7XHHK.js +30 -0
- package/dist/ghBotCli-7WDYF4L7.js +677 -0
- package/dist/integrationCli-ZMTSILYF.js +685 -0
- package/dist/login-W4Y6WHLV.js +16 -0
- package/dist/{mcpCli-Q2H63ISP.js → mcpCli-5QISTOFK.js} +42 -9
- package/dist/modelsCli-FZMBD4OE.js +36 -0
- package/dist/npmRunner-LU43GM3W.js +46 -0
- package/dist/oauthLogin-VESMXG4M.js +210 -0
- package/dist/pluginsCli-GEHR7FQR.js +284 -0
- package/dist/prCli-JXA7DJW4.js +72 -0
- package/dist/providerCli-EUZGKP5F.js +15 -0
- package/dist/sdk.js +5486 -0
- package/dist/serve-6KX4MXHC.js +178 -0
- package/dist/serve-YSUJD57D.js +515 -0
- package/dist/session-F6MG3UCU.js +12 -0
- package/dist/setup-LLWREJQH.js +24 -0
- package/dist/statsCli-H4HEY5ID.js +353 -0
- package/dist/transferCli-2XUKBG2L.js +71 -0
- package/dist/ui-6HJVYI46.js +2370 -0
- package/dist/update-O4WCLKOO.js +109 -0
- package/package.json +24 -3
- package/dist/args-FCLPQ7TO.js +0 -11
- package/dist/chunk-5O4HD7IU.js +0 -253
- package/dist/chunk-C5AFWX7H.js +0 -328
- package/dist/chunk-GW36UMHZ.js +0 -52
- package/dist/chunk-RZUJYAQV.js +0 -112
- package/dist/chunk-UN7QXPCB.js +0 -82
- package/dist/login-F4G5QXOU.js +0 -9
- package/dist/providerCli-MXV6PD6F.js +0 -11
- package/dist/session-7UB77CEG.js +0 -9
- package/dist/ui-RYTSKEIA.js +0 -1344
package/dist/cli.js
CHANGED
|
@@ -1,1337 +1,213 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./chunk-
|
|
3
|
+
resolveWorktree,
|
|
4
|
+
runPrint
|
|
5
|
+
} from "./chunk-TQWCM5RO.js";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
createRuntime
|
|
8
|
+
} from "./chunk-KIZC5XV4.js";
|
|
9
|
+
import "./chunk-QZKLZELZ.js";
|
|
10
|
+
import "./chunk-YIYX3FFS.js";
|
|
11
|
+
import "./chunk-DP7EBW53.js";
|
|
12
|
+
import "./chunk-PSO7R5AD.js";
|
|
11
13
|
import {
|
|
12
|
-
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
VERSION
|
|
15
|
+
} from "./chunk-VU5HNQHX.js";
|
|
14
16
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
parseFrontmatter,
|
|
22
|
-
runPrint
|
|
23
|
-
} from "./chunk-5O4HD7IU.js";
|
|
17
|
+
helpText,
|
|
18
|
+
parseCliArgs
|
|
19
|
+
} from "./chunk-FTFVW7XD.js";
|
|
20
|
+
import "./chunk-GAFJEQTJ.js";
|
|
21
|
+
import "./chunk-N7KP6RNP.js";
|
|
22
|
+
import "./chunk-QXOV4DUZ.js";
|
|
24
23
|
import {
|
|
25
|
-
|
|
26
|
-
} from "./chunk-
|
|
24
|
+
readGlobalConfigFile
|
|
25
|
+
} from "./chunk-2QYHSESB.js";
|
|
27
26
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
defaultLoginDeps,
|
|
28
|
+
runLogin
|
|
29
|
+
} from "./chunk-KD2LMIS4.js";
|
|
30
|
+
import "./chunk-7F3IWBPQ.js";
|
|
30
31
|
import {
|
|
31
|
-
Session,
|
|
32
32
|
pruneSessions
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-F6ACN47V.js";
|
|
34
|
+
import "./chunk-R4BJBTWC.js";
|
|
35
|
+
import "./chunk-SNKVS26H.js";
|
|
36
|
+
import "./chunk-H73FO5YK.js";
|
|
34
37
|
import {
|
|
35
38
|
ConfigError,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// src/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
setTheme
|
|
40
|
+
} from "./chunk-NDEJUL44.js";
|
|
41
|
+
import {
|
|
42
|
+
resolveLanguage,
|
|
43
|
+
setLanguage,
|
|
44
|
+
t
|
|
45
|
+
} from "./chunk-XLC6T4SX.js";
|
|
46
|
+
|
|
47
|
+
// src/exit.ts
|
|
48
|
+
var EXIT_GRACE_MS = 1e3;
|
|
49
|
+
function settleExit(code, deps) {
|
|
50
|
+
const d = {
|
|
51
|
+
setExitCode: (c) => {
|
|
52
|
+
process.exitCode = c;
|
|
53
|
+
},
|
|
54
|
+
exit: (c) => process.exit(c),
|
|
55
|
+
setTimer: (fn, ms) => setTimeout(fn, ms),
|
|
56
|
+
...deps
|
|
57
|
+
};
|
|
58
|
+
d.setExitCode(code);
|
|
59
|
+
const force = d.setTimer(() => d.exit(code), EXIT_GRACE_MS);
|
|
60
|
+
force.unref();
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
// src/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (platform === "win32") {
|
|
57
|
-
const killer = spawnFn("taskkill", ["/pid", String(pid), "/T", "/F"]);
|
|
58
|
-
killer.on("error", () => {
|
|
59
|
-
try {
|
|
60
|
-
process.kill(pid);
|
|
61
|
-
} catch {
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
63
|
+
// src/cli.ts
|
|
64
|
+
function chooseLanguage(argv) {
|
|
65
|
+
let fromConfig;
|
|
66
66
|
try {
|
|
67
|
-
process.
|
|
67
|
+
fromConfig = readGlobalConfigFile(process.env).language;
|
|
68
68
|
} catch {
|
|
69
|
-
try {
|
|
70
|
-
process.kill(pid, "SIGKILL");
|
|
71
|
-
} catch {
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
var active = /* @__PURE__ */ new Set();
|
|
76
|
-
var exitHookInstalled = false;
|
|
77
|
-
function killAllCommands() {
|
|
78
|
-
for (const kill of active) kill();
|
|
79
|
-
}
|
|
80
|
-
function runCommand(command, opts) {
|
|
81
|
-
return new Promise((resolve2) => {
|
|
82
|
-
const child = spawn(command, {
|
|
83
|
-
cwd: opts.cwd,
|
|
84
|
-
shell: true,
|
|
85
|
-
detached: process.platform !== "win32",
|
|
86
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
87
|
-
});
|
|
88
|
-
if (!exitHookInstalled) {
|
|
89
|
-
process.on("exit", killAllCommands);
|
|
90
|
-
exitHookInstalled = true;
|
|
91
|
-
}
|
|
92
|
-
child.stdout.setEncoding("utf8");
|
|
93
|
-
child.stderr.setEncoding("utf8");
|
|
94
|
-
let head = "";
|
|
95
|
-
let tail = "";
|
|
96
|
-
let dropped = 0;
|
|
97
|
-
let timedOut = false;
|
|
98
|
-
let aborted = false;
|
|
99
|
-
const onData = (data) => {
|
|
100
|
-
let s = data;
|
|
101
|
-
if (head.length < HALF) {
|
|
102
|
-
const take = s.slice(0, HALF - head.length);
|
|
103
|
-
head += take;
|
|
104
|
-
s = s.slice(take.length);
|
|
105
|
-
}
|
|
106
|
-
if (s) {
|
|
107
|
-
tail += s;
|
|
108
|
-
if (tail.length > HALF) {
|
|
109
|
-
dropped += tail.length - HALF;
|
|
110
|
-
tail = tail.slice(-HALF);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
child.stdout.on("data", onData);
|
|
115
|
-
child.stderr.on("data", onData);
|
|
116
|
-
const kill = () => {
|
|
117
|
-
if (child.pid !== void 0) killProcessTree(child.pid);
|
|
118
|
-
};
|
|
119
|
-
const killAsAborted = () => {
|
|
120
|
-
aborted = true;
|
|
121
|
-
kill();
|
|
122
|
-
};
|
|
123
|
-
active.add(killAsAborted);
|
|
124
|
-
const timer = setTimeout(() => {
|
|
125
|
-
timedOut = true;
|
|
126
|
-
kill();
|
|
127
|
-
}, opts.timeoutMs);
|
|
128
|
-
const onAbort = () => {
|
|
129
|
-
aborted = true;
|
|
130
|
-
kill();
|
|
131
|
-
};
|
|
132
|
-
if (opts.signal.aborted) onAbort();
|
|
133
|
-
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
134
|
-
const finish = (exitCode, extra = "") => {
|
|
135
|
-
active.delete(killAsAborted);
|
|
136
|
-
clearTimeout(timer);
|
|
137
|
-
opts.signal.removeEventListener("abort", onAbort);
|
|
138
|
-
const output = dropped ? `${head}
|
|
139
|
-
\u2026 [${dropped} karakter dipotong] \u2026
|
|
140
|
-
${tail}` : head + tail;
|
|
141
|
-
resolve2({ output: output + extra, exitCode, timedOut, aborted });
|
|
142
|
-
};
|
|
143
|
-
child.on("error", (error) => finish(null, error.message));
|
|
144
|
-
child.on("close", (code) => finish(code));
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
var bashTool = defineTool({
|
|
148
|
-
name: "bash",
|
|
149
|
-
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.",
|
|
150
|
-
schema: z.object({
|
|
151
|
-
command: z.string().describe("The shell command to run"),
|
|
152
|
-
timeout_ms: z.number().int().min(1e3).max(6e5).optional().describe("Timeout in milliseconds")
|
|
153
|
-
}),
|
|
154
|
-
kind: "bash",
|
|
155
|
-
target: (input) => input.command,
|
|
156
|
-
async run(input, ctx) {
|
|
157
|
-
const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
|
|
158
|
-
const status = r.timedOut ? `[dihentikan: timeout ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} detik]` : r.aborted ? "[dibatalkan user]" : `[exit code ${r.exitCode}]`;
|
|
159
|
-
return { output: `${r.output.trimEnd()}
|
|
160
|
-
${status}`.trimStart(), isError: r.exitCode !== 0 };
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// src/tools/edit.ts
|
|
165
|
-
import { readFile, writeFile } from "fs/promises";
|
|
166
|
-
import { z as z2 } from "zod";
|
|
167
|
-
|
|
168
|
-
// src/diff.ts
|
|
169
|
-
import { structuredPatch } from "diff";
|
|
170
|
-
function diffLines(oldText, newText) {
|
|
171
|
-
const patch = structuredPatch("a", "b", oldText, newText, "", "", { context: 2 });
|
|
172
|
-
const out = [];
|
|
173
|
-
for (const hunk of patch.hunks) {
|
|
174
|
-
let oldLine = hunk.oldStart;
|
|
175
|
-
let newLine = hunk.newStart;
|
|
176
|
-
for (const raw of hunk.lines) {
|
|
177
|
-
const text = raw.slice(1);
|
|
178
|
-
if (raw[0] === "-") out.push({ kind: "del", line: oldLine++, text });
|
|
179
|
-
else if (raw[0] === "+") out.push({ kind: "add", line: newLine++, text });
|
|
180
|
-
else if (raw[0] === " ") {
|
|
181
|
-
out.push({ kind: "ctx", line: newLine, text });
|
|
182
|
-
oldLine++;
|
|
183
|
-
newLine++;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return out;
|
|
188
|
-
}
|
|
189
|
-
function formatDiff(lines) {
|
|
190
|
-
const mark = { add: "+", del: "-", ctx: " " };
|
|
191
|
-
return lines.map((l) => `${String(l.line).padStart(5)} ${mark[l.kind]} ${l.text}`).join("\n");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// src/tools/paths.ts
|
|
195
|
-
import { isAbsolute, resolve } from "path";
|
|
196
|
-
function resolvePath(cwd, path) {
|
|
197
|
-
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
198
|
-
}
|
|
199
|
-
function errorCode(error) {
|
|
200
|
-
return error.code ?? error.message;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// src/tools/edit.ts
|
|
204
|
-
var schema = z2.object({
|
|
205
|
-
path: z2.string().describe("File path, relative to the project or absolute"),
|
|
206
|
-
old_string: z2.string().describe("Exact text to replace; must be unique unless replace_all"),
|
|
207
|
-
new_string: z2.string().describe("Replacement text"),
|
|
208
|
-
replace_all: z2.boolean().optional().describe("Replace every occurrence")
|
|
209
|
-
});
|
|
210
|
-
function countOccurrences(haystack, needle) {
|
|
211
|
-
let count = 0;
|
|
212
|
-
for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) count++;
|
|
213
|
-
return count;
|
|
214
|
-
}
|
|
215
|
-
function applyEdit(content, input) {
|
|
216
|
-
if (input.old_string === input.new_string) {
|
|
217
|
-
return { output: "old_string dan new_string sama; tidak ada perubahan.", isError: true };
|
|
218
|
-
}
|
|
219
|
-
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
220
|
-
const toEol = (s) => s.replace(/\r?\n/g, eol);
|
|
221
|
-
const attempts = [[input.old_string, input.new_string]];
|
|
222
|
-
if (toEol(input.old_string) !== input.old_string) attempts.push([toEol(input.old_string), toEol(input.new_string)]);
|
|
223
|
-
for (const [oldText, newText] of attempts) {
|
|
224
|
-
const count = oldText ? countOccurrences(content, oldText) : 0;
|
|
225
|
-
if (count === 0) continue;
|
|
226
|
-
if (count > 1 && !input.replace_all) {
|
|
227
|
-
return {
|
|
228
|
-
output: `old_string muncul ${count} kali di ${input.path}. Tambah konteks agar unik, atau pakai replace_all.`,
|
|
229
|
-
isError: true
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
|
|
233
|
-
}
|
|
234
|
-
return { output: `old_string tidak ditemukan di ${input.path}. Baca ulang file-nya.`, isError: true };
|
|
235
|
-
}
|
|
236
|
-
var editTool = defineTool({
|
|
237
|
-
name: "edit",
|
|
238
|
-
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.",
|
|
239
|
-
schema,
|
|
240
|
-
kind: "edit",
|
|
241
|
-
target: (input) => input.path,
|
|
242
|
-
async validate(input, ctx) {
|
|
243
|
-
const abs = resolvePath(ctx.cwd, input.path);
|
|
244
|
-
if (!ctx.readFiles.has(abs)) return `Baca ${input.path} dulu dengan read sebelum mengedit.`;
|
|
245
|
-
let content;
|
|
246
|
-
try {
|
|
247
|
-
content = await readFile(abs, "utf8");
|
|
248
|
-
} catch (error) {
|
|
249
|
-
return `Tidak bisa membaca ${input.path}: ${errorCode(error)}`;
|
|
250
|
-
}
|
|
251
|
-
const next = applyEdit(content, input);
|
|
252
|
-
return typeof next === "string" ? void 0 : next.output;
|
|
253
|
-
},
|
|
254
|
-
async preview(input, ctx) {
|
|
255
|
-
const abs = resolvePath(ctx.cwd, input.path);
|
|
256
|
-
const content = await readFile(abs, "utf8");
|
|
257
|
-
const next = applyEdit(content, input);
|
|
258
|
-
return typeof next === "string" ? formatDiff(diffLines(content, next)) : void 0;
|
|
259
|
-
},
|
|
260
|
-
async run(input, ctx) {
|
|
261
|
-
const abs = resolvePath(ctx.cwd, input.path);
|
|
262
|
-
if (!ctx.readFiles.has(abs)) {
|
|
263
|
-
return { output: `Baca ${input.path} dulu dengan read sebelum mengedit.`, isError: true };
|
|
264
|
-
}
|
|
265
|
-
let content;
|
|
266
|
-
try {
|
|
267
|
-
content = await readFile(abs, "utf8");
|
|
268
|
-
} catch (error) {
|
|
269
|
-
return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
|
|
270
|
-
}
|
|
271
|
-
const next = applyEdit(content, input);
|
|
272
|
-
if (typeof next !== "string") return next;
|
|
273
|
-
await ctx.checkpoint?.(abs);
|
|
274
|
-
await writeFile(abs, next);
|
|
275
|
-
return { output: `Mengedit ${input.path}.`, display: formatDiff(diffLines(content, next)) };
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// src/tools/fetch.ts
|
|
280
|
-
import { z as z3 } from "zod";
|
|
281
|
-
var MAX_CHARS = 5e4;
|
|
282
|
-
var MAX_BYTES = 5 * 1024 * 1024;
|
|
283
|
-
async function readCapped(res) {
|
|
284
|
-
const reader = res.body?.getReader();
|
|
285
|
-
if (!reader) return { text: "", truncated: false };
|
|
286
|
-
const chunks = [];
|
|
287
|
-
let size = 0;
|
|
288
|
-
for (; ; ) {
|
|
289
|
-
const { done, value } = await reader.read();
|
|
290
|
-
if (done) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
|
|
291
|
-
size += value.length;
|
|
292
|
-
if (size > MAX_BYTES) {
|
|
293
|
-
chunks.push(value.subarray(0, value.length - (size - MAX_BYTES)));
|
|
294
|
-
await reader.cancel();
|
|
295
|
-
return { text: Buffer.concat(chunks).toString("utf8"), truncated: true };
|
|
296
|
-
}
|
|
297
|
-
chunks.push(value);
|
|
298
69
|
}
|
|
70
|
+
setLanguage(resolveLanguage(argv, process.env, fromConfig));
|
|
299
71
|
}
|
|
300
|
-
function htmlToText(html) {
|
|
301
|
-
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");
|
|
302
|
-
}
|
|
303
|
-
var fetchTool = defineTool({
|
|
304
|
-
name: "fetch",
|
|
305
|
-
description: "Fetch a URL (http/https). HTML is converted to plain text. Max 50k characters.",
|
|
306
|
-
schema: z3.object({ url: z3.string().describe("Absolute http(s) URL") }),
|
|
307
|
-
kind: "fetch",
|
|
308
|
-
target: (input) => input.url,
|
|
309
|
-
async run(input, ctx) {
|
|
310
|
-
if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
|
|
311
|
-
let url = new URL(input.url);
|
|
312
|
-
let res;
|
|
313
|
-
try {
|
|
314
|
-
for (let hop = 0; hop < 5; hop++) {
|
|
315
|
-
res = await fetch(url, { signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
|
|
316
|
-
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
317
|
-
if (!location) break;
|
|
318
|
-
const next = new URL(location, url);
|
|
319
|
-
if (next.host !== url.host) {
|
|
320
|
-
await res.body?.cancel();
|
|
321
|
-
return {
|
|
322
|
-
output: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).`,
|
|
323
|
-
isError: true
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
await res.body?.cancel();
|
|
327
|
-
url = next;
|
|
328
|
-
}
|
|
329
|
-
} catch (error) {
|
|
330
|
-
return { output: `Gagal mengambil ${input.url}: ${error.message}`, isError: true };
|
|
331
|
-
}
|
|
332
|
-
if (!res) return { output: `Gagal mengambil ${input.url}`, isError: true };
|
|
333
|
-
const { text: body, truncated } = await readCapped(res);
|
|
334
|
-
if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${body.slice(0, 500)}`, isError: true };
|
|
335
|
-
const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
|
|
336
|
-
const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
|
|
337
|
-
\u2026 [dipotong]` : text;
|
|
338
|
-
const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
339
|
-
return { output: clipped, display };
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// src/tools/glob.ts
|
|
344
|
-
import fg from "fast-glob";
|
|
345
|
-
import { z as z4 } from "zod";
|
|
346
|
-
var globTool = defineTool({
|
|
347
|
-
name: "glob",
|
|
348
|
-
description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
|
|
349
|
-
schema: z4.object({
|
|
350
|
-
pattern: z4.string().describe("Glob pattern"),
|
|
351
|
-
path: z4.string().optional().describe("Base directory, default project root")
|
|
352
|
-
}),
|
|
353
|
-
kind: "read",
|
|
354
|
-
target: (input) => input.pattern,
|
|
355
|
-
async run(input, ctx) {
|
|
356
|
-
const root = resolvePath(ctx.cwd, input.path ?? ".");
|
|
357
|
-
const files = (await fg(input.pattern, { cwd: root, ignore: ignorePatterns(root), onlyFiles: true, dot: true })).sort();
|
|
358
|
-
if (!files.length) return { output: "Tidak ada file yang cocok." };
|
|
359
|
-
const shown = files.slice(0, 500);
|
|
360
|
-
const more = files.length > shown.length ? `
|
|
361
|
-
\u2026 ${files.length - shown.length} file lagi` : "";
|
|
362
|
-
return { output: shown.join("\n") + more, display: `${files.length} file` };
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// src/tools/grep.ts
|
|
367
|
-
import { execFile } from "child_process";
|
|
368
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
369
|
-
import fg2 from "fast-glob";
|
|
370
|
-
import { z as z5 } from "zod";
|
|
371
|
-
var MAX_MATCHES = 200;
|
|
372
|
-
async function grepJs(pattern, opts) {
|
|
373
|
-
const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
|
|
374
|
-
const files = await fg2(opts.glob ?? "**/*", { cwd: opts.root, ignore: ignorePatterns(opts.root), onlyFiles: true, dot: true });
|
|
375
|
-
const out = [];
|
|
376
|
-
for (const file of files.sort()) {
|
|
377
|
-
let text;
|
|
378
|
-
try {
|
|
379
|
-
const buffer = await readFile2(resolvePath(opts.root, file));
|
|
380
|
-
if (buffer.subarray(0, 8e3).includes(0)) continue;
|
|
381
|
-
text = buffer.toString("utf8");
|
|
382
|
-
} catch {
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
const lines = text.split(/\r?\n/);
|
|
386
|
-
for (let i = 0; i < lines.length && out.length < MAX_MATCHES; i++) {
|
|
387
|
-
if (regex.test(lines[i])) out.push(`${file}:${i + 1}:${lines[i].slice(0, 300)}`);
|
|
388
|
-
}
|
|
389
|
-
if (out.length >= MAX_MATCHES) break;
|
|
390
|
-
}
|
|
391
|
-
return out;
|
|
392
|
-
}
|
|
393
|
-
function grepRg(pattern, opts) {
|
|
394
|
-
const args = ["--line-number", "--no-heading", "--color", "never", "--max-columns", "300", "-e", pattern];
|
|
395
|
-
if (opts.ignoreCase) args.push("-i");
|
|
396
|
-
if (opts.glob) args.push("-g", opts.glob);
|
|
397
|
-
args.push(".");
|
|
398
|
-
return new Promise((resolve2) => {
|
|
399
|
-
execFile("rg", args, { cwd: opts.root, maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
|
|
400
|
-
const code = error?.code;
|
|
401
|
-
if (code === "ENOENT") return resolve2(void 0);
|
|
402
|
-
if (error && error.code !== 1) return resolve2(void 0);
|
|
403
|
-
resolve2(stdout.split("\n").filter(Boolean).map((l) => l.replace(/^\.\//, "")).slice(0, MAX_MATCHES));
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
var grepTool = defineTool({
|
|
408
|
-
name: "grep",
|
|
409
|
-
description: "Search file contents with a regular expression. Respects .gitignore. Returns path:line:text (max 200).",
|
|
410
|
-
schema: z5.object({
|
|
411
|
-
pattern: z5.string().describe("Regular expression"),
|
|
412
|
-
path: z5.string().optional().describe("Directory to search, default project root"),
|
|
413
|
-
glob: z5.string().optional().describe('Only files matching this glob, e.g. "**/*.ts"'),
|
|
414
|
-
ignore_case: z5.boolean().optional()
|
|
415
|
-
}),
|
|
416
|
-
kind: "read",
|
|
417
|
-
target: (input) => input.pattern,
|
|
418
|
-
async run(input, ctx) {
|
|
419
|
-
const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case };
|
|
420
|
-
try {
|
|
421
|
-
new RegExp(input.pattern);
|
|
422
|
-
} catch (error) {
|
|
423
|
-
return { output: `Regex tidak valid: ${error.message}`, isError: true };
|
|
424
|
-
}
|
|
425
|
-
const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
|
|
426
|
-
if (!matches.length) return { output: "Tidak ada yang cocok." };
|
|
427
|
-
return { output: matches.join("\n"), display: `${matches.length} hasil` };
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// src/tools/read.ts
|
|
432
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
433
|
-
import { z as z6 } from "zod";
|
|
434
|
-
var MAX_LINE = 2e3;
|
|
435
|
-
var readTool = defineTool({
|
|
436
|
-
name: "read",
|
|
437
|
-
description: "Read a text file. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it.",
|
|
438
|
-
schema: z6.object({
|
|
439
|
-
path: z6.string().describe("File path, relative to the project or absolute"),
|
|
440
|
-
offset: z6.number().int().min(1).optional().describe("First line to read, 1-based"),
|
|
441
|
-
limit: z6.number().int().min(1).optional().describe("Number of lines, default 2000")
|
|
442
|
-
}),
|
|
443
|
-
kind: "read",
|
|
444
|
-
target: (input) => input.path,
|
|
445
|
-
async run(input, ctx) {
|
|
446
|
-
const abs = resolvePath(ctx.cwd, input.path);
|
|
447
|
-
let buffer;
|
|
448
|
-
try {
|
|
449
|
-
buffer = await readFile3(abs);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
return { output: `Tidak bisa membaca ${input.path}: ${errorCode(error)}`, isError: true };
|
|
452
|
-
}
|
|
453
|
-
if (buffer.subarray(0, 8e3).includes(0)) {
|
|
454
|
-
return { output: `${input.path} adalah file biner, tidak dibaca.`, isError: true };
|
|
455
|
-
}
|
|
456
|
-
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
457
|
-
const start = (input.offset ?? 1) - 1;
|
|
458
|
-
const limit = input.limit ?? 2e3;
|
|
459
|
-
const slice = lines.slice(start, start + limit);
|
|
460
|
-
const body = slice.map((line, i) => {
|
|
461
|
-
const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [baris dipotong]` : line;
|
|
462
|
-
return `${String(start + i + 1).padStart(6)} ${text}`;
|
|
463
|
-
}).join("\n");
|
|
464
|
-
const remaining = lines.length - start - slice.length;
|
|
465
|
-
ctx.readFiles.add(abs);
|
|
466
|
-
return {
|
|
467
|
-
output: remaining > 0 ? `${body}
|
|
468
|
-
\u2026 ${remaining} baris lagi (pakai offset)` : body,
|
|
469
|
-
display: `${slice.length} baris`
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// src/tools/write.ts
|
|
475
|
-
import { existsSync } from "fs";
|
|
476
|
-
import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
477
|
-
import { dirname } from "path";
|
|
478
|
-
import { z as z7 } from "zod";
|
|
479
|
-
var writeTool = defineTool({
|
|
480
|
-
name: "write",
|
|
481
|
-
description: "Create or overwrite a file with the given content. Read an existing file first. Prefer edit for changes.",
|
|
482
|
-
schema: z7.object({
|
|
483
|
-
path: z7.string().describe("File path, relative to the project or absolute"),
|
|
484
|
-
content: z7.string().describe("Full new file content")
|
|
485
|
-
}),
|
|
486
|
-
kind: "edit",
|
|
487
|
-
target: (input) => input.path,
|
|
488
|
-
async validate(input, ctx) {
|
|
489
|
-
const abs = resolvePath(ctx.cwd, input.path);
|
|
490
|
-
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.` : void 0;
|
|
491
|
-
},
|
|
492
|
-
async preview(input, ctx) {
|
|
493
|
-
const abs = resolvePath(ctx.cwd, input.path);
|
|
494
|
-
const old = existsSync(abs) ? await readFile4(abs, "utf8") : "";
|
|
495
|
-
return formatDiff(diffLines(old, input.content));
|
|
496
|
-
},
|
|
497
|
-
async run(input, ctx) {
|
|
498
|
-
const abs = resolvePath(ctx.cwd, input.path);
|
|
499
|
-
if (existsSync(abs) && !ctx.readFiles.has(abs)) {
|
|
500
|
-
return { output: `${input.path} sudah ada. Baca dulu dengan read sebelum menimpanya.`, isError: true };
|
|
501
|
-
}
|
|
502
|
-
try {
|
|
503
|
-
await ctx.checkpoint?.(abs);
|
|
504
|
-
await mkdir(dirname(abs), { recursive: true });
|
|
505
|
-
await writeFile2(abs, input.content);
|
|
506
|
-
} catch (error) {
|
|
507
|
-
return { output: `Gagal menulis ${input.path}: ${errorCode(error)}`, isError: true };
|
|
508
|
-
}
|
|
509
|
-
ctx.readFiles.add(abs);
|
|
510
|
-
const lines = input.content.split("\n").length;
|
|
511
|
-
return { output: `Menulis ${input.path} (${lines} baris).`, display: `${lines} baris ditulis` };
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
// src/tools/index.ts
|
|
516
|
-
var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fetchTool];
|
|
517
|
-
function toolDefinitions(tools) {
|
|
518
|
-
return tools.map((tool) => {
|
|
519
|
-
const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z8.toJSONSchema(tool.schema);
|
|
520
|
-
return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// src/textToolCalls.ts
|
|
525
|
-
var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
|
|
526
|
-
var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
|
|
527
|
-
function coerce(value, type) {
|
|
528
|
-
const types = Array.isArray(type) ? type : [type];
|
|
529
|
-
const v = value.trim();
|
|
530
|
-
if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
|
|
531
|
-
if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
|
|
532
|
-
if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
|
|
533
|
-
try {
|
|
534
|
-
return JSON.parse(v);
|
|
535
|
-
} catch {
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return value.replace(/^\n+|\n+$/g, "");
|
|
539
|
-
}
|
|
540
|
-
function pickTool(name, keys, tools) {
|
|
541
|
-
const named = name && tools.find((t) => t.function.name === name);
|
|
542
|
-
if (named) return named;
|
|
543
|
-
let best;
|
|
544
|
-
let bestScore = 0;
|
|
545
|
-
let tie = false;
|
|
546
|
-
for (const t of tools) {
|
|
547
|
-
const params = t.function.parameters;
|
|
548
|
-
const props = Object.keys(params.properties ?? {});
|
|
549
|
-
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
550
|
-
const score = keys.filter((k) => props.includes(k)).length;
|
|
551
|
-
if (score > bestScore) [best, bestScore, tie] = [t, score, false];
|
|
552
|
-
else if (score === bestScore && score > 0) tie = true;
|
|
553
|
-
}
|
|
554
|
-
return tie ? void 0 : best;
|
|
555
|
-
}
|
|
556
|
-
function recoverTextToolCalls(text, tools) {
|
|
557
|
-
if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
|
|
558
|
-
const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
|
|
559
|
-
const firstPair = text.search(PAIR);
|
|
560
|
-
const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
|
|
561
|
-
const bounds = starts.filter((i) => i >= cut);
|
|
562
|
-
if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
|
|
563
|
-
const toolCalls = [];
|
|
564
|
-
bounds.forEach((start, n) => {
|
|
565
|
-
const segment = text.slice(start, bounds[n + 1] ?? text.length);
|
|
566
|
-
const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
|
|
567
|
-
let args = [];
|
|
568
|
-
const flush = () => {
|
|
569
|
-
if (!args.length) return;
|
|
570
|
-
const keys = args.map(([k]) => k);
|
|
571
|
-
const tool = pickTool(name, keys, tools);
|
|
572
|
-
if (tool) {
|
|
573
|
-
const props = tool.function.parameters.properties ?? {};
|
|
574
|
-
const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
|
|
575
|
-
toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
|
|
576
|
-
}
|
|
577
|
-
args = [];
|
|
578
|
-
};
|
|
579
|
-
for (const m of segment.matchAll(PAIR)) {
|
|
580
|
-
if (args.some(([k]) => k === m[1])) flush();
|
|
581
|
-
args.push([m[1], m[2]]);
|
|
582
|
-
}
|
|
583
|
-
flush();
|
|
584
|
-
});
|
|
585
|
-
if (!toolCalls.length) return null;
|
|
586
|
-
return { text: text.slice(0, cut).trim(), toolCalls };
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// src/agent.ts
|
|
590
|
-
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
591
|
-
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.";
|
|
592
|
-
var Agent = class {
|
|
593
|
-
constructor(opts) {
|
|
594
|
-
this.opts = opts;
|
|
595
|
-
this.provider = opts.provider;
|
|
596
|
-
this.permissions = opts.permissions;
|
|
597
|
-
this.tools = opts.tools;
|
|
598
|
-
this.definitions = toolDefinitions(opts.tools);
|
|
599
|
-
this.turnTools = this.tools;
|
|
600
|
-
this.turnDefinitions = this.definitions;
|
|
601
|
-
this.messages = [...opts.history ?? []];
|
|
602
|
-
this.maxSteps = opts.maxSteps ?? 50;
|
|
603
|
-
this.contextWindow = opts.contextWindow ?? 128e3;
|
|
604
|
-
this.systemPrompt = opts.systemPrompt;
|
|
605
|
-
}
|
|
606
|
-
opts;
|
|
607
|
-
provider;
|
|
608
|
-
permissions;
|
|
609
|
-
messages;
|
|
610
|
-
totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
611
|
-
lastInputTokens = 0;
|
|
612
|
-
onEvent = () => {
|
|
613
|
-
};
|
|
614
|
-
askPermission = async () => "no";
|
|
615
|
-
tools;
|
|
616
|
-
definitions;
|
|
617
|
-
// Snapshot for the running turn: tools added/removed mid-turn (e.g. an MCP server connecting) apply from the next turn.
|
|
618
|
-
turnTools;
|
|
619
|
-
turnDefinitions;
|
|
620
|
-
readFiles = /* @__PURE__ */ new Set();
|
|
621
|
-
maxSteps;
|
|
622
|
-
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
623
|
-
contextWindow;
|
|
624
|
-
systemPrompt;
|
|
625
|
-
setSystemPrompt(text) {
|
|
626
|
-
this.systemPrompt = text;
|
|
627
|
-
}
|
|
628
|
-
setTools(tools) {
|
|
629
|
-
this.tools = tools;
|
|
630
|
-
this.definitions = toolDefinitions(tools);
|
|
631
|
-
}
|
|
632
|
-
isParallelSafe(call) {
|
|
633
|
-
const tool = this.turnTools.find((t) => t.name === call.name);
|
|
634
|
-
if (!tool?.parallelSafe) return false;
|
|
635
|
-
try {
|
|
636
|
-
const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
|
|
637
|
-
return parsed.success && tool.parallelSafe(parsed.data);
|
|
638
|
-
} catch {
|
|
639
|
-
return false;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
push(message) {
|
|
643
|
-
this.messages.push(message);
|
|
644
|
-
this.opts.onMessage?.(message);
|
|
645
|
-
}
|
|
646
|
-
/** Swaps in another conversation (resume, new session) without writing to the session log. */
|
|
647
|
-
load(messages) {
|
|
648
|
-
this.messages = messages;
|
|
649
|
-
this.readFiles.clear();
|
|
650
|
-
this.lastInputTokens = 0;
|
|
651
|
-
}
|
|
652
|
-
clear() {
|
|
653
|
-
this.messages = [];
|
|
654
|
-
this.readFiles.clear();
|
|
655
|
-
this.lastInputTokens = 0;
|
|
656
|
-
this.opts.onReset?.();
|
|
657
|
-
}
|
|
658
|
-
async compact(signal) {
|
|
659
|
-
const completion = await this.provider.chat({
|
|
660
|
-
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
|
|
661
|
-
// Some gateways reject tool_calls in history when no tools are declared.
|
|
662
|
-
tools: this.definitions,
|
|
663
|
-
signal
|
|
664
|
-
});
|
|
665
|
-
this.clear();
|
|
666
|
-
this.push({ role: "user", content: `Ringkasan percakapan sebelumnya:
|
|
667
|
-
${completion.text}` });
|
|
668
|
-
this.push({ role: "assistant", content: "Oke, saya lanjutkan dari ringkasan ini." });
|
|
669
|
-
this.onEvent({ type: "compacted" });
|
|
670
|
-
}
|
|
671
|
-
async run(text, signal) {
|
|
672
|
-
this.turnTools = this.tools;
|
|
673
|
-
this.turnDefinitions = this.definitions;
|
|
674
|
-
this.opts.onTurnStart?.();
|
|
675
|
-
try {
|
|
676
|
-
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
677
|
-
this.push({ role: "user", content: text });
|
|
678
|
-
for (let step = 0; step < this.maxSteps; step++) {
|
|
679
|
-
if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
|
|
680
|
-
await this.compact(signal);
|
|
681
|
-
this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
|
|
682
|
-
}
|
|
683
|
-
let completion = await this.provider.chat({
|
|
684
|
-
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
|
|
685
|
-
tools: this.turnDefinitions,
|
|
686
|
-
signal,
|
|
687
|
-
onText: (delta) => this.onEvent({ type: "text", delta }),
|
|
688
|
-
onThinking: (delta) => this.onEvent({ type: "thinking", delta })
|
|
689
|
-
});
|
|
690
|
-
if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
|
|
691
|
-
const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
|
|
692
|
-
if (recovered) {
|
|
693
|
-
completion = { ...completion, ...recovered };
|
|
694
|
-
this.onEvent({ type: "textReplace", text: recovered.text });
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
const usage = completion.usage ?? {
|
|
698
|
-
inputTokens: estimateTokens(this.systemPrompt) + estimateTokens(JSON.stringify(this.messages)),
|
|
699
|
-
outputTokens: estimateTokens(completion.text) + (completion.toolCalls.length ? estimateTokens(JSON.stringify(completion.toolCalls)) : 0)
|
|
700
|
-
};
|
|
701
|
-
this.totalUsage.inputTokens += usage.inputTokens;
|
|
702
|
-
this.totalUsage.outputTokens += usage.outputTokens;
|
|
703
|
-
this.lastInputTokens = usage.inputTokens;
|
|
704
|
-
this.onEvent({ type: "usage", ...this.totalUsage });
|
|
705
|
-
this.push({
|
|
706
|
-
role: "assistant",
|
|
707
|
-
content: completion.text || null,
|
|
708
|
-
...completion.toolCalls.length ? {
|
|
709
|
-
tool_calls: completion.toolCalls.map((c) => ({
|
|
710
|
-
id: c.id,
|
|
711
|
-
type: "function",
|
|
712
|
-
function: { name: c.name, arguments: c.arguments }
|
|
713
|
-
}))
|
|
714
|
-
} : {}
|
|
715
|
-
});
|
|
716
|
-
if (completion.finishReason === "repetition") {
|
|
717
|
-
this.onEvent({ type: "textReplace", text: completion.text });
|
|
718
|
-
this.onEvent({
|
|
719
|
-
type: "error",
|
|
720
|
-
message: "Model terjebak mengulang teks, jawaban dihentikan. Coba ulangi atau ganti model dengan /model."
|
|
721
|
-
});
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
if (!completion.toolCalls.length) {
|
|
725
|
-
this.onEvent({ type: "done" });
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
const calls = completion.toolCalls;
|
|
729
|
-
let i = 0;
|
|
730
|
-
while (i < calls.length) {
|
|
731
|
-
if (signal.aborted) {
|
|
732
|
-
for (const pending of calls.slice(i)) this.push({ role: "tool", tool_call_id: pending.id, content: "Dibatalkan oleh user." });
|
|
733
|
-
this.onEvent({ type: "aborted" });
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
let j = i + 1;
|
|
737
|
-
if (this.isParallelSafe(calls[i])) while (j < calls.length && this.isParallelSafe(calls[j])) j++;
|
|
738
|
-
const batch = calls.slice(i, j);
|
|
739
|
-
const results = await Promise.all(batch.map((c) => this.runTool(c, signal)));
|
|
740
|
-
batch.forEach((c, k) => {
|
|
741
|
-
this.push({ role: "tool", tool_call_id: c.id, content: results[k] });
|
|
742
|
-
});
|
|
743
|
-
i = j;
|
|
744
|
-
}
|
|
745
|
-
if (signal.aborted) {
|
|
746
|
-
this.onEvent({ type: "aborted" });
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
this.onEvent({ type: "stepLimit" });
|
|
751
|
-
} catch (error) {
|
|
752
|
-
if (signal.aborted) {
|
|
753
|
-
this.onEvent({ type: "aborted" });
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
const message = error.message;
|
|
757
|
-
const noTools = error.status === 400 && /tool/i.test(message);
|
|
758
|
-
this.onEvent({
|
|
759
|
-
type: "error",
|
|
760
|
-
message: noTools ? `${message}
|
|
761
|
-
Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model.` : message
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
async runTool(call, signal) {
|
|
766
|
-
const fail = (output, target2 = "") => {
|
|
767
|
-
this.onEvent({ type: "toolStart", id: call.id, tool: call.name, target: target2 });
|
|
768
|
-
this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
|
|
769
|
-
return output;
|
|
770
|
-
};
|
|
771
|
-
const tool = this.turnTools.find((t) => t.name === call.name);
|
|
772
|
-
if (!tool) return fail(`Alat "${call.name}" tidak ada. Alat yang tersedia: ${this.turnTools.map((t) => t.name).join(", ")}`);
|
|
773
|
-
let args;
|
|
774
|
-
try {
|
|
775
|
-
args = JSON.parse(call.arguments || "{}");
|
|
776
|
-
} catch {
|
|
777
|
-
return fail(`Argumen bukan JSON valid: ${call.arguments.slice(0, 200)}`);
|
|
778
|
-
}
|
|
779
|
-
const parsed = tool.schema.safeParse(args);
|
|
780
|
-
if (!parsed.success) {
|
|
781
|
-
return fail(`Argumen tidak valid: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
782
|
-
}
|
|
783
|
-
const input = parsed.data;
|
|
784
|
-
const target = tool.target(input);
|
|
785
|
-
const ctx = {
|
|
786
|
-
cwd: this.opts.cwd,
|
|
787
|
-
signal,
|
|
788
|
-
readFiles: this.readFiles,
|
|
789
|
-
callId: call.id,
|
|
790
|
-
emit: (event) => this.onEvent(event),
|
|
791
|
-
ask: (req) => this.askPermission(req),
|
|
792
|
-
checkpoint: this.opts.checkpoint,
|
|
793
|
-
addUsage: (u) => {
|
|
794
|
-
this.totalUsage.inputTokens += u.inputTokens;
|
|
795
|
-
this.totalUsage.outputTokens += u.outputTokens;
|
|
796
|
-
this.onEvent({ type: "usage", ...this.totalUsage });
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
this.onEvent({ type: "toolStart", id: call.id, tool: tool.name, target });
|
|
800
|
-
const invalid = await tool.validate?.(input, ctx).catch((error) => error.message);
|
|
801
|
-
if (invalid) {
|
|
802
|
-
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: invalid, isError: true });
|
|
803
|
-
return invalid;
|
|
804
|
-
}
|
|
805
|
-
const request = { tool: tool.name, kind: tool.kind, target };
|
|
806
|
-
let decision = this.permissions.check(request);
|
|
807
|
-
if (decision === "ask") {
|
|
808
|
-
const preview = await tool.preview?.(input, ctx).catch(() => void 0);
|
|
809
|
-
const answer = await this.askPermission({
|
|
810
|
-
...request,
|
|
811
|
-
preview,
|
|
812
|
-
sessionRules: this.permissions.rulesFor(request),
|
|
813
|
-
...this.opts.label ? { agent: this.opts.label } : {}
|
|
814
|
-
});
|
|
815
|
-
if (answer === "session") this.permissions.allowForSession(request);
|
|
816
|
-
if (answer === "all") this.permissions.mode = "allowAll";
|
|
817
|
-
decision = answer === "no" ? "deny" : "allow";
|
|
818
|
-
}
|
|
819
|
-
if (decision === "deny") {
|
|
820
|
-
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.";
|
|
821
|
-
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
|
|
822
|
-
return output;
|
|
823
|
-
}
|
|
824
|
-
let result;
|
|
825
|
-
try {
|
|
826
|
-
result = await tool.run(input, ctx);
|
|
827
|
-
} catch (error) {
|
|
828
|
-
result = { output: `Error: ${error.message}`, isError: true };
|
|
829
|
-
}
|
|
830
|
-
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
|
|
831
|
-
return result.output;
|
|
832
|
-
}
|
|
833
|
-
};
|
|
834
|
-
|
|
835
|
-
// src/checkpoints.ts
|
|
836
|
-
import { existsSync as existsSync2, statSync } from "fs";
|
|
837
|
-
import { readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
|
|
838
|
-
var MAX_TURNS = 20;
|
|
839
|
-
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
840
|
-
var CheckpointStore = class {
|
|
841
|
-
history = [];
|
|
842
|
-
current;
|
|
843
|
-
beginTurn() {
|
|
844
|
-
if (this.current?.size === 0) return;
|
|
845
|
-
this.current = /* @__PURE__ */ new Map();
|
|
846
|
-
this.history.push(this.current);
|
|
847
|
-
}
|
|
848
|
-
/** Call before writing a file; only the first snapshot per file per turn is kept. */
|
|
849
|
-
async snapshot(path) {
|
|
850
|
-
if (!this.current) this.beginTurn();
|
|
851
|
-
const turn = this.current;
|
|
852
|
-
if (turn.has(path)) return;
|
|
853
|
-
if (!existsSync2(path)) turn.set(path, null);
|
|
854
|
-
else if (statSync(path).size > MAX_BYTES2) turn.set(path, "skipped");
|
|
855
|
-
else turn.set(path, await readFile5(path));
|
|
856
|
-
while (this.turns() > MAX_TURNS) this.history.shift();
|
|
857
|
-
}
|
|
858
|
-
turns() {
|
|
859
|
-
return this.history.filter((t) => t.size).length;
|
|
860
|
-
}
|
|
861
|
-
async undo() {
|
|
862
|
-
while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
|
|
863
|
-
const turn = this.history.pop();
|
|
864
|
-
this.current = void 0;
|
|
865
|
-
if (!turn) return void 0;
|
|
866
|
-
const result = { restored: [], deleted: [], skipped: [], failed: [] };
|
|
867
|
-
for (const [path, original] of turn) {
|
|
868
|
-
try {
|
|
869
|
-
if (original === "skipped") result.skipped.push(path);
|
|
870
|
-
else if (original === null) {
|
|
871
|
-
await rm(path, { force: true });
|
|
872
|
-
result.deleted.push(path);
|
|
873
|
-
} else {
|
|
874
|
-
await writeFile3(path, original);
|
|
875
|
-
result.restored.push(path);
|
|
876
|
-
}
|
|
877
|
-
} catch {
|
|
878
|
-
result.failed.push(path);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
return result;
|
|
882
|
-
}
|
|
883
|
-
clear() {
|
|
884
|
-
this.history.length = 0;
|
|
885
|
-
this.current = void 0;
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
// src/mcp/manager.ts
|
|
890
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
891
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
892
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
893
|
-
import { z as z9 } from "zod";
|
|
894
|
-
function mcpToolName(server, tool, taken) {
|
|
895
|
-
const clean = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
896
|
-
const base = `mcp__${clean(server)}__${clean(tool)}`.slice(0, 64);
|
|
897
|
-
let name = base;
|
|
898
|
-
for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
|
|
899
|
-
taken.add(name);
|
|
900
|
-
return name;
|
|
901
|
-
}
|
|
902
|
-
function withTimeout(promise, ms) {
|
|
903
|
-
let timer;
|
|
904
|
-
const timeout = new Promise((_, reject) => {
|
|
905
|
-
timer = setTimeout(() => reject(new Error(`timeout ${Math.round(ms / 1e3)} detik`)), ms);
|
|
906
|
-
});
|
|
907
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
908
|
-
}
|
|
909
|
-
var McpManager = class {
|
|
910
|
-
constructor(opts = {}) {
|
|
911
|
-
this.opts = opts;
|
|
912
|
-
}
|
|
913
|
-
opts;
|
|
914
|
-
servers = /* @__PURE__ */ new Map();
|
|
915
|
-
taken = /* @__PURE__ */ new Set();
|
|
916
|
-
async start(specs) {
|
|
917
|
-
await Promise.all(specs.map((spec) => this.add(spec)));
|
|
918
|
-
}
|
|
919
|
-
async add(spec) {
|
|
920
|
-
await this.remove(spec.name);
|
|
921
|
-
const entry = { spec, state: { name: spec.name, source: spec.source, status: "starting", tools: 0 }, tools: [] };
|
|
922
|
-
this.servers.set(spec.name, entry);
|
|
923
|
-
this.opts.onChange?.();
|
|
924
|
-
const client = new Client({ name: "bccli", version: "0.3.0" });
|
|
925
|
-
entry.client = client;
|
|
926
|
-
const timeoutMs = this.opts.connectTimeoutMs ?? 3e4;
|
|
927
|
-
try {
|
|
928
|
-
const config = spec.config;
|
|
929
|
-
const transport = config.type === "http" ? new StreamableHTTPClientTransport(new URL(config.url), { requestInit: { headers: config.headers ?? {} } }) : new StdioClientTransport({
|
|
930
|
-
command: config.command,
|
|
931
|
-
args: config.args ?? [],
|
|
932
|
-
env: { ...process.env, ...config.env },
|
|
933
|
-
stderr: "ignore"
|
|
934
|
-
});
|
|
935
|
-
await withTimeout(client.connect(transport), timeoutMs);
|
|
936
|
-
const { tools } = await withTimeout(client.listTools(), timeoutMs);
|
|
937
|
-
if (this.servers.get(spec.name) !== entry) {
|
|
938
|
-
await client.close().catch(() => {
|
|
939
|
-
});
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
entry.tools = tools.map((t) => this.wrap(spec.name, client, t));
|
|
943
|
-
entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
|
|
944
|
-
} catch (error) {
|
|
945
|
-
entry.state = { ...entry.state, status: "error", error: error.message };
|
|
946
|
-
await client.close().catch(() => {
|
|
947
|
-
});
|
|
948
|
-
if (this.servers.get(spec.name) !== entry) return;
|
|
949
|
-
}
|
|
950
|
-
this.opts.onChange?.();
|
|
951
|
-
}
|
|
952
|
-
wrap(server, client, tool) {
|
|
953
|
-
return {
|
|
954
|
-
name: mcpToolName(server, tool.name, this.taken),
|
|
955
|
-
description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
|
|
956
|
-
schema: z9.record(z9.string(), z9.unknown()),
|
|
957
|
-
jsonSchema: tool.inputSchema,
|
|
958
|
-
kind: "mcp",
|
|
959
|
-
target: (input) => {
|
|
960
|
-
const text = JSON.stringify(input);
|
|
961
|
-
return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
962
|
-
},
|
|
963
|
-
async run(input, ctx) {
|
|
964
|
-
try {
|
|
965
|
-
const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
|
|
966
|
-
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(kosong)";
|
|
967
|
-
return { output: output.length > 5e4 ? `${output.slice(0, 5e4)}
|
|
968
|
-
\u2026 [dipotong]` : output, isError: !!result.isError };
|
|
969
|
-
} catch (error) {
|
|
970
|
-
return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
async remove(name) {
|
|
976
|
-
const entry = this.servers.get(name);
|
|
977
|
-
if (!entry) return;
|
|
978
|
-
this.servers.delete(name);
|
|
979
|
-
for (const t of entry.tools) this.taken.delete(t.name);
|
|
980
|
-
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t) => t.name));
|
|
981
|
-
await entry.client?.close().catch(() => {
|
|
982
|
-
});
|
|
983
|
-
this.opts.onChange?.();
|
|
984
|
-
}
|
|
985
|
-
tools() {
|
|
986
|
-
return [...this.servers.values()].flatMap((s) => s.tools);
|
|
987
|
-
}
|
|
988
|
-
states() {
|
|
989
|
-
return [...this.servers.values()].map((s) => s.state);
|
|
990
|
-
}
|
|
991
|
-
async stop() {
|
|
992
|
-
await Promise.all([...this.servers.keys()].map((name) => this.remove(name)));
|
|
993
|
-
}
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
// src/setup.ts
|
|
997
|
-
import { homedir } from "os";
|
|
998
|
-
|
|
999
|
-
// src/tools/plan.ts
|
|
1000
|
-
import { z as z10 } from "zod";
|
|
1001
|
-
function createExitPlanTool(opts) {
|
|
1002
|
-
return defineTool({
|
|
1003
|
-
name: "exit_plan",
|
|
1004
|
-
description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
|
|
1005
|
-
schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
|
|
1006
|
-
kind: "read",
|
|
1007
|
-
target: () => "rencana",
|
|
1008
|
-
async run(input) {
|
|
1009
|
-
if (opts.permissions.mode !== "plan") return { output: "exit_plan hanya dipakai di mode plan.", isError: true };
|
|
1010
|
-
const decision = await opts.interaction.approvePlan(input.plan);
|
|
1011
|
-
if (decision === "no") {
|
|
1012
|
-
return { output: "User belum menyetujui. Tanyakan apa yang perlu diperbaiki, perbaiki rencananya, lalu panggil exit_plan lagi." };
|
|
1013
|
-
}
|
|
1014
|
-
opts.permissions.mode = decision;
|
|
1015
|
-
return { output: `Rencana disetujui. Mode sekarang ${decision}. Mulai kerjakan rencananya sekarang.` };
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// src/tools/skill.ts
|
|
1021
|
-
import { readFileSync } from "fs";
|
|
1022
|
-
import { z as z11 } from "zod";
|
|
1023
|
-
function createSkillTool(skills) {
|
|
1024
|
-
return defineTool({
|
|
1025
|
-
name: "skill",
|
|
1026
|
-
description: "Load a skill (instructions for a kind of task) by name. Use it when a listed skill matches the task, then follow it.",
|
|
1027
|
-
schema: z11.object({ name: z11.string().describe("Skill name from the list in the system prompt") }),
|
|
1028
|
-
kind: "read",
|
|
1029
|
-
target: (input) => input.name,
|
|
1030
|
-
async run(input) {
|
|
1031
|
-
const skill = skills.find((s) => s.name === input.name);
|
|
1032
|
-
if (!skill) return { output: `Skill "${input.name}" tidak ada. Tersedia: ${skills.map((s) => s.name).join(", ") || "(tidak ada)"}`, isError: true };
|
|
1033
|
-
const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
|
|
1034
|
-
return { output: `Base directory for this skill: ${skill.dir}
|
|
1035
|
-
|
|
1036
|
-
${body}`, display: skill.description.slice(0, 60) };
|
|
1037
|
-
}
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// src/tools/task.ts
|
|
1042
|
-
import { z as z12 } from "zod";
|
|
1043
|
-
function createTaskTool(opts) {
|
|
1044
|
-
const byName = new Map(opts.agents.map((a) => [a.name, a]));
|
|
1045
|
-
const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
1046
|
-
return defineTool({
|
|
1047
|
-
name: "task",
|
|
1048
|
-
description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
|
|
1049
|
-
${list}`,
|
|
1050
|
-
schema: z12.object({
|
|
1051
|
-
agent: z12.string().describe("Agent name"),
|
|
1052
|
-
description: z12.string().describe("3-6 word summary shown to the user"),
|
|
1053
|
-
prompt: z12.string().describe("Full instructions; the subagent sees nothing else")
|
|
1054
|
-
}),
|
|
1055
|
-
kind: "read",
|
|
1056
|
-
target: (input) => `[${input.agent}] ${input.description}`,
|
|
1057
|
-
// Parallel only when the resolved agent can do nothing but read (a project file may redefine "explore").
|
|
1058
|
-
parallelSafe: (input) => {
|
|
1059
|
-
const allowed = byName.get(input.agent)?.tools;
|
|
1060
|
-
if (!allowed) return false;
|
|
1061
|
-
const base = opts.baseTools();
|
|
1062
|
-
return allowed.every((name) => base.find((t) => t.name === name)?.kind === "read");
|
|
1063
|
-
},
|
|
1064
|
-
async run(input, ctx) {
|
|
1065
|
-
const def = byName.get(input.agent);
|
|
1066
|
-
if (!def) return { output: `Agent "${input.agent}" tidak ada. Tersedia: ${[...byName.keys()].join(", ")}`, isError: true };
|
|
1067
|
-
const base = opts.baseTools().filter((t) => t.name !== "task" && t.name !== "exit_plan");
|
|
1068
|
-
const unknown = (def.tools ?? []).filter((n) => !base.some((t) => t.name === n));
|
|
1069
|
-
if (unknown.length) return { output: `Agent "${def.name}": alat tidak dikenal: ${unknown.join(", ")}`, isError: true };
|
|
1070
|
-
const allowed = def.tools;
|
|
1071
|
-
const tools = allowed ? base.filter((t) => allowed.includes(t.name)) : base;
|
|
1072
|
-
let provider;
|
|
1073
|
-
try {
|
|
1074
|
-
provider = def.model ? opts.providerFor(def.model) : opts.provider();
|
|
1075
|
-
} catch (error) {
|
|
1076
|
-
return { output: `Agent "${def.name}": ${error.message}`, isError: true };
|
|
1077
|
-
}
|
|
1078
|
-
const child = new Agent({
|
|
1079
|
-
provider,
|
|
1080
|
-
tools,
|
|
1081
|
-
permissions: opts.permissions,
|
|
1082
|
-
systemPrompt: `${opts.systemPrompt}
|
|
1083
|
-
|
|
1084
|
-
# Your role
|
|
1085
|
-
${def.prompt}`,
|
|
1086
|
-
cwd: opts.cwd,
|
|
1087
|
-
maxSteps: def.maxSteps ?? 50,
|
|
1088
|
-
label: def.name,
|
|
1089
|
-
// Subagent edits belong to the parent's turn so /undo reverts them too.
|
|
1090
|
-
checkpoint: ctx.checkpoint
|
|
1091
|
-
});
|
|
1092
|
-
let outcome;
|
|
1093
|
-
let steps = 0;
|
|
1094
|
-
child.onEvent = (event) => {
|
|
1095
|
-
if (event.type === "toolStart") steps++;
|
|
1096
|
-
if (event.type === "error" || event.type === "stepLimit" || event.type === "aborted" || event.type === "done") outcome = event;
|
|
1097
|
-
if (event.type !== "usage" && ctx.callId) ctx.emit?.({ type: "subagent", parentId: ctx.callId, agent: def.name, event });
|
|
1098
|
-
};
|
|
1099
|
-
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1100
|
-
await child.run(input.prompt, ctx.signal);
|
|
1101
|
-
ctx.addUsage?.(child.totalUsage);
|
|
1102
|
-
const tokens = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1103
|
-
const display = `${steps} langkah \xB7 ${tokens >= 1e3 ? `${Math.round(tokens / 1e3)}k` : tokens} token`;
|
|
1104
|
-
if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
|
|
1105
|
-
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
|
|
1106
|
-
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
1107
|
-
const report = last && typeof last.content === "string" ? last.content : "(tidak ada laporan)";
|
|
1108
|
-
const suffix = outcome?.type === "stepLimit" ? `
|
|
1109
|
-
|
|
1110
|
-
(Subagent berhenti di batas ${def.maxSteps ?? 50} langkah.)` : "";
|
|
1111
|
-
return { output: report + suffix, display };
|
|
1112
|
-
}
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// src/tools/todo.ts
|
|
1117
|
-
import { z as z13 } from "zod";
|
|
1118
|
-
var TodoStore = class {
|
|
1119
|
-
items = [];
|
|
1120
|
-
listeners = /* @__PURE__ */ new Set();
|
|
1121
|
-
set(items) {
|
|
1122
|
-
this.items = items;
|
|
1123
|
-
for (const fn of this.listeners) fn(items);
|
|
1124
|
-
}
|
|
1125
|
-
subscribe(fn) {
|
|
1126
|
-
this.listeners.add(fn);
|
|
1127
|
-
return () => this.listeners.delete(fn);
|
|
1128
|
-
}
|
|
1129
|
-
};
|
|
1130
|
-
function createTodoTool(store) {
|
|
1131
|
-
return defineTool({
|
|
1132
|
-
name: "todo_write",
|
|
1133
|
-
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.",
|
|
1134
|
-
schema: z13.object({
|
|
1135
|
-
todos: z13.array(z13.object({ content: z13.string(), status: z13.enum(["pending", "in_progress", "completed"]) }))
|
|
1136
|
-
}),
|
|
1137
|
-
kind: "read",
|
|
1138
|
-
target: (input) => `${input.todos.length} item`,
|
|
1139
|
-
async run(input) {
|
|
1140
|
-
if (input.todos.filter((t) => t.status === "in_progress").length > 1) {
|
|
1141
|
-
return { output: "Hanya boleh satu item in_progress.", isError: true };
|
|
1142
|
-
}
|
|
1143
|
-
store.set(input.todos);
|
|
1144
|
-
const done = input.todos.filter((t) => t.status === "completed").length;
|
|
1145
|
-
return { output: `Todo diperbarui: ${done}/${input.todos.length} selesai.` };
|
|
1146
|
-
}
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// src/setup.ts
|
|
1151
|
-
var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
|
|
1152
|
-
function createRuntime(opts) {
|
|
1153
|
-
const env = opts.env ?? process.env;
|
|
1154
|
-
const home = bccliHome(env);
|
|
1155
|
-
let config = loadConfig(opts.cwd, env);
|
|
1156
|
-
let modelRef = opts.args.model ?? config.model;
|
|
1157
|
-
const makeProvider = (ref) => {
|
|
1158
|
-
const resolved = resolveModel(config, ref, env);
|
|
1159
|
-
return opts.provider ?? createProvider({ baseURL: resolved.baseURL, apiKey: resolved.apiKey, model: resolved.model });
|
|
1160
|
-
};
|
|
1161
|
-
const provider = makeProvider(modelRef);
|
|
1162
|
-
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
1163
|
-
const rules = [...config.allow, ...opts.args.allowedTools.map((t) => TOOL_RULES[t] ?? t)];
|
|
1164
|
-
const permissions = new Permissions(mode, rules, opts.cwd);
|
|
1165
|
-
const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
|
|
1166
|
-
const skills = loadSkills(roots);
|
|
1167
|
-
const commands = loadCommands(roots);
|
|
1168
|
-
const agentDefs = loadAgentDefs(roots);
|
|
1169
|
-
const todos = new TodoStore();
|
|
1170
|
-
const checkpoints = new CheckpointStore();
|
|
1171
|
-
let startedAt = /* @__PURE__ */ new Date();
|
|
1172
|
-
const interaction = { approvePlan: async () => "no" };
|
|
1173
|
-
let session = Session.create(home, opts.cwd);
|
|
1174
|
-
let history = opts.history;
|
|
1175
|
-
if (!history && opts.args.continue) {
|
|
1176
|
-
const latest = Session.latest(home, opts.cwd);
|
|
1177
|
-
if (latest) {
|
|
1178
|
-
session = latest;
|
|
1179
|
-
history = latest.load();
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
|
|
1183
|
-
const baseTools = [...ALL_TOOLS, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
|
|
1184
|
-
const task = createTaskTool({
|
|
1185
|
-
agents: [...BUILTIN_AGENTS, ...agentDefs],
|
|
1186
|
-
// Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
|
|
1187
|
-
baseTools: () => agent.tools.filter((t) => t.name !== "task"),
|
|
1188
|
-
permissions,
|
|
1189
|
-
provider: () => agent.provider,
|
|
1190
|
-
providerFor: makeProvider,
|
|
1191
|
-
systemPrompt,
|
|
1192
|
-
cwd: opts.cwd
|
|
1193
|
-
});
|
|
1194
|
-
const agent = new Agent({
|
|
1195
|
-
provider,
|
|
1196
|
-
tools: [...baseTools, task],
|
|
1197
|
-
permissions,
|
|
1198
|
-
systemPrompt,
|
|
1199
|
-
cwd: opts.cwd,
|
|
1200
|
-
history,
|
|
1201
|
-
onMessage: (m) => session.append(m),
|
|
1202
|
-
onReset: () => session.reset(),
|
|
1203
|
-
onTurnStart: () => checkpoints.beginTurn(),
|
|
1204
|
-
checkpoint: (path) => checkpoints.snapshot(path)
|
|
1205
|
-
});
|
|
1206
|
-
const mcp = new McpManager({
|
|
1207
|
-
onChange: () => agent.setTools([...baseTools, task, ...mcp.tools()]),
|
|
1208
|
-
// A later server may reuse a sanitized name; it must not inherit this one's "[a]" grants.
|
|
1209
|
-
onToolsRemoved: (names) => {
|
|
1210
|
-
for (const name of names) permissions.revoke(`mcp(${name})`);
|
|
1211
|
-
}
|
|
1212
|
-
});
|
|
1213
|
-
return {
|
|
1214
|
-
cwd: opts.cwd,
|
|
1215
|
-
home,
|
|
1216
|
-
env,
|
|
1217
|
-
skills,
|
|
1218
|
-
commands,
|
|
1219
|
-
agentDefs,
|
|
1220
|
-
todos,
|
|
1221
|
-
interaction,
|
|
1222
|
-
mcp,
|
|
1223
|
-
startMcp: (specs) => mcp.start(specs),
|
|
1224
|
-
get config() {
|
|
1225
|
-
return config;
|
|
1226
|
-
},
|
|
1227
|
-
reloadConfig() {
|
|
1228
|
-
config = loadConfig(opts.cwd, env);
|
|
1229
|
-
},
|
|
1230
|
-
providerLabel() {
|
|
1231
|
-
return providerName(config, modelRef.slice(0, modelRef.indexOf("/")));
|
|
1232
|
-
},
|
|
1233
|
-
listModels: (only) => listAllModels(config, env, { fetch: opts.fetch, only }),
|
|
1234
|
-
addProviderKey(id, key) {
|
|
1235
|
-
return addProviderKey(id, key, { env, cwd: opts.cwd, out: () => {
|
|
1236
|
-
}, err: () => {
|
|
1237
|
-
}, readSecret: async () => "", fetch: opts.fetch });
|
|
1238
|
-
},
|
|
1239
|
-
get modelRef() {
|
|
1240
|
-
return modelRef;
|
|
1241
|
-
},
|
|
1242
|
-
agent,
|
|
1243
|
-
get session() {
|
|
1244
|
-
return session;
|
|
1245
|
-
},
|
|
1246
|
-
resume(previous) {
|
|
1247
|
-
session = previous;
|
|
1248
|
-
agent.load(previous.load());
|
|
1249
|
-
agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1250
|
-
todos.set([]);
|
|
1251
|
-
checkpoints.clear();
|
|
1252
|
-
},
|
|
1253
|
-
newSession() {
|
|
1254
|
-
session = Session.create(home, opts.cwd);
|
|
1255
|
-
agent.load([]);
|
|
1256
|
-
agent.totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1257
|
-
todos.set([]);
|
|
1258
|
-
checkpoints.clear();
|
|
1259
|
-
startedAt = /* @__PURE__ */ new Date();
|
|
1260
|
-
},
|
|
1261
|
-
rebuildSystemPrompt() {
|
|
1262
|
-
agent.setSystemPrompt(buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills }));
|
|
1263
|
-
},
|
|
1264
|
-
checkpoints,
|
|
1265
|
-
get startedAt() {
|
|
1266
|
-
return startedAt;
|
|
1267
|
-
},
|
|
1268
|
-
setModel(ref) {
|
|
1269
|
-
agent.provider = makeProvider(ref);
|
|
1270
|
-
modelRef = ref;
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// src/version.ts
|
|
1276
|
-
var VERSION = true ? "0.4.0-beta.5" : "0.0.0-dev";
|
|
1277
|
-
|
|
1278
|
-
// src/cli.ts
|
|
1279
72
|
async function main() {
|
|
73
|
+
chooseLanguage(process.argv.slice(2));
|
|
1280
74
|
const args = parseCliArgs(process.argv.slice(2));
|
|
1281
75
|
if (args.version) {
|
|
1282
76
|
console.log(VERSION);
|
|
1283
77
|
return 0;
|
|
1284
78
|
}
|
|
1285
79
|
if (args.help) {
|
|
1286
|
-
console.log(
|
|
80
|
+
console.log(helpText());
|
|
1287
81
|
return 0;
|
|
1288
82
|
}
|
|
1289
|
-
|
|
1290
|
-
if (args.
|
|
83
|
+
let cwd = process.cwd();
|
|
84
|
+
if (args.worktree) cwd = (await resolveWorktree(cwd, args.worktree)).path;
|
|
85
|
+
if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd, args, defaultLoginDeps());
|
|
1291
86
|
if (args.command === "mcp") {
|
|
1292
|
-
const { runMcpCommand } = await import("./mcpCli-
|
|
1293
|
-
const { readSecret } = await import("./login-
|
|
87
|
+
const { runMcpCommand } = await import("./mcpCli-5QISTOFK.js");
|
|
88
|
+
const { readSecret } = await import("./login-W4Y6WHLV.js");
|
|
1294
89
|
return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1295
90
|
}
|
|
1296
91
|
if (args.command === "provider") {
|
|
1297
|
-
const { runProviderCommand } = await import("./providerCli-
|
|
1298
|
-
const { readSecret } = await import("./login-
|
|
92
|
+
const { runProviderCommand } = await import("./providerCli-EUZGKP5F.js");
|
|
93
|
+
const { readSecret } = await import("./login-W4Y6WHLV.js");
|
|
1299
94
|
return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1300
95
|
}
|
|
96
|
+
if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
|
|
97
|
+
const { runIntegrationCommand } = await import("./integrationCli-ZMTSILYF.js");
|
|
98
|
+
return runIntegrationCommand(args, {
|
|
99
|
+
env: process.env,
|
|
100
|
+
cwd,
|
|
101
|
+
out: (s) => console.log(s),
|
|
102
|
+
err: (s) => console.error(s),
|
|
103
|
+
fetch: globalThis.fetch
|
|
104
|
+
});
|
|
105
|
+
}
|
|
1301
106
|
if (args.command === "models") {
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
107
|
+
const { runModelsCommand } = await import("./modelsCli-FZMBD4OE.js");
|
|
108
|
+
return runModelsCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), fetch: globalThis.fetch });
|
|
109
|
+
}
|
|
110
|
+
if (args.command === "update") {
|
|
111
|
+
const { runUpdateCommand } = await import("./update-O4WCLKOO.js");
|
|
112
|
+
return runUpdateCommand({ env: process.env });
|
|
113
|
+
}
|
|
114
|
+
if (args.command === "acp") {
|
|
115
|
+
const { runtimeAgent, runAcp } = await import("./acp-NWP4FFTG.js");
|
|
116
|
+
await runAcp(process.stdin, process.stdout, { cwd, newAgent: (c) => runtimeAgent(c, args.allowAll) });
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (args.command === "serve") {
|
|
120
|
+
const { startServe } = await import("./serve-YSUJD57D.js");
|
|
121
|
+
const handle = await startServe({ port: args.port, host: args.host, token: args.token, cwd, env: process.env });
|
|
122
|
+
process.stderr.write(`bccli serve ${handle.url} \u2014 Authorization: Bearer ${handle.token}
|
|
123
|
+
`);
|
|
124
|
+
await new Promise((resolve) => {
|
|
125
|
+
const stop = () => {
|
|
126
|
+
void handle.close().finally(resolve);
|
|
127
|
+
};
|
|
128
|
+
process.once("SIGINT", stop);
|
|
129
|
+
process.once("SIGTERM", stop);
|
|
130
|
+
});
|
|
1306
131
|
return 0;
|
|
1307
132
|
}
|
|
133
|
+
if (args.command === "plugins") {
|
|
134
|
+
const { runPluginsCommand } = await import("./pluginsCli-GEHR7FQR.js");
|
|
135
|
+
const { readGlobalPlugins, updateGlobalConfig } = await import("./config-H5A7XHHK.js");
|
|
136
|
+
const { runNpmSync } = await import("./npmRunner-LU43GM3W.js");
|
|
137
|
+
return runPluginsCommand(args, {
|
|
138
|
+
out: (s) => console.log(s),
|
|
139
|
+
err: (s) => console.error(s),
|
|
140
|
+
env: process.env,
|
|
141
|
+
runNpm: async (prefix, argv) => runNpmSync(prefix, argv),
|
|
142
|
+
readPlugins: () => readGlobalPlugins(process.env),
|
|
143
|
+
savePlugins: (entries) => updateGlobalConfig(process.env, (cfg) => {
|
|
144
|
+
cfg.plugins = entries;
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (args.command === "gh-bot") {
|
|
149
|
+
const { runGhBotCommand } = await import("./ghBotCli-7WDYF4L7.js");
|
|
150
|
+
return runGhBotCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s) });
|
|
151
|
+
}
|
|
152
|
+
if (args.command === "completion") {
|
|
153
|
+
const { runCompletionCommand } = await import("./completion-OV3MW2N4.js");
|
|
154
|
+
return runCompletionCommand(args, { out: (s) => console.log(s), err: (s) => console.error(s) });
|
|
155
|
+
}
|
|
156
|
+
if (args.command === "stats") {
|
|
157
|
+
const { runStatsCommand } = await import("./statsCli-H4HEY5ID.js");
|
|
158
|
+
const { bccliHome } = await import("./config-H5A7XHHK.js");
|
|
159
|
+
return runStatsCommand(
|
|
160
|
+
{ subArgs: args.subArgs, home: bccliHome(process.env), cwd, days: args.days, tools: args.tools, models: args.models, project: args.project },
|
|
161
|
+
{ out: (s) => console.log(s), err: (s) => console.error(s) }
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (args.command === "export" || args.command === "import") {
|
|
165
|
+
const { runExportCommand, runImportCommand } = await import("./transferCli-2XUKBG2L.js");
|
|
166
|
+
const { bccliHome } = await import("./config-H5A7XHHK.js");
|
|
167
|
+
const deps = { subArgs: args.subArgs, home: bccliHome(process.env), cwd };
|
|
168
|
+
const io = { out: (s) => console.log(s), err: (s) => console.error(s) };
|
|
169
|
+
return args.command === "export" ? runExportCommand(deps, io) : runImportCommand(deps, io);
|
|
170
|
+
}
|
|
171
|
+
if (args.command === "pr") {
|
|
172
|
+
const { runPrCommand } = await import("./prCli-JXA7DJW4.js");
|
|
173
|
+
return runPrCommand({ subArgs: args.subArgs, cwd }, { out: (s) => console.log(s), err: (s) => console.error(s) });
|
|
174
|
+
}
|
|
1308
175
|
const rt = createRuntime({ cwd, args });
|
|
176
|
+
setTheme(rt.config.theme);
|
|
1309
177
|
try {
|
|
1310
|
-
pruneSessions(rt.home);
|
|
178
|
+
if (rt.config.sessionRetentionDays) pruneSessions(rt.home, Date.now(), rt.config.sessionRetentionDays);
|
|
1311
179
|
} catch {
|
|
1312
180
|
}
|
|
1313
181
|
if (args.print) {
|
|
1314
|
-
if (!args.prompt) throw new ConfigError('
|
|
1315
|
-
const { serversToStart } = await import("./config-
|
|
182
|
+
if (!args.prompt) throw new ConfigError(t('-p mode needs a task, e.g. bccli -p "explain this repo"'));
|
|
183
|
+
const { serversToStart } = await import("./config-AZZFOXKV.js");
|
|
1316
184
|
const plan = serversToStart(rt.home, cwd);
|
|
1317
185
|
for (const s of plan.needTrust) {
|
|
1318
|
-
console.error(
|
|
186
|
+
console.error(t('Project MCP server "{name}" skipped (not approved yet; run interactive bccli once to approve it).', { name: s.name }));
|
|
1319
187
|
}
|
|
1320
188
|
await rt.startMcp(plan.start);
|
|
1321
189
|
try {
|
|
1322
|
-
return await runPrint(rt, args.prompt, void 0, { allowAll: args.allowAll });
|
|
190
|
+
return await runPrint(rt, args.prompt, void 0, { allowAll: args.allowAll, outputFormat: args.outputFormat });
|
|
1323
191
|
} finally {
|
|
1324
192
|
await rt.mcp.stop();
|
|
1325
193
|
}
|
|
1326
194
|
}
|
|
1327
|
-
if (!process.stdin.isTTY) throw new ConfigError('
|
|
1328
|
-
|
|
195
|
+
if (!process.stdin.isTTY) throw new ConfigError(t('Interactive mode needs a terminal. For scripts/CI use: bccli -p "task"'));
|
|
196
|
+
if (rt.config.updateCheck !== "off" && !process.env.BCCLI_NO_UPDATE_CHECK) {
|
|
197
|
+
try {
|
|
198
|
+
const { checkForUpdate } = await import("./update-O4WCLKOO.js");
|
|
199
|
+
const update = await checkForUpdate({ home: rt.home, fetch: globalThis.fetch, now: /* @__PURE__ */ new Date() });
|
|
200
|
+
if (update) console.error(update);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const { startInteractive } = await import("./ui-6HJVYI46.js");
|
|
1329
205
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1330
206
|
}
|
|
1331
207
|
main().then(
|
|
1332
|
-
(code) =>
|
|
208
|
+
(code) => settleExit(code),
|
|
1333
209
|
(error) => {
|
|
1334
210
|
console.error(error instanceof ConfigError || error?.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? error.message : error);
|
|
1335
|
-
|
|
211
|
+
settleExit(1);
|
|
1336
212
|
}
|
|
1337
213
|
);
|