@botconnector/bccli 0.4.0-beta.11 → 0.4.0-beta.13
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
CHANGED
|
@@ -117,7 +117,8 @@ endpoint lain, BCCLI menolak menimpanya. Cline sendiri menyimpan API key provide
|
|
|
117
117
|
|
|
118
118
|
DeepSeek Harness memakai custom provider `botconnector` di `$DSH_HOME/settings.yaml` dengan
|
|
119
119
|
`api: openai-completions` dan `apiKeyEnv: BOTCONNECTOR_API_KEY`; key diletakkan pada blok BCCLI di
|
|
120
|
-
`$DSH_HOME/.env`. Provider DSH lain tetap dipertahankan.
|
|
120
|
+
`$DSH_HOME/.env`. Provider DSH lain tetap dipertahankan. Setelah connect, jalankan profile web dengan
|
|
121
|
+
`dsh web` (setara dengan `dsh --profile web`); `dsh` tanpa profile memang ditolak oleh DSH 0.1.x.
|
|
121
122
|
|
|
122
123
|
Codex memakai profile `$CODEX_HOME/botconnector.config.toml` dengan `wire_api = "responses"` dan launcher
|
|
123
124
|
BCCLI yang membaca key dari storage BCCLI saat dijalankan. Claude Code memakai launcher terisolasi dengan
|
package/dist/cli.js
CHANGED
|
@@ -39,6 +39,71 @@ import {
|
|
|
39
39
|
resolveModel
|
|
40
40
|
} from "./chunk-2MGINW4O.js";
|
|
41
41
|
|
|
42
|
+
// src/textToolCalls.ts
|
|
43
|
+
var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
|
|
44
|
+
var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
|
|
45
|
+
function coerce(value, type) {
|
|
46
|
+
const types = Array.isArray(type) ? type : [type];
|
|
47
|
+
const v = value.trim();
|
|
48
|
+
if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
|
|
49
|
+
if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
|
|
50
|
+
if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(v);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return value.replace(/^\n+|\n+$/g, "");
|
|
57
|
+
}
|
|
58
|
+
function pickTool(name, keys, tools) {
|
|
59
|
+
const named = name && tools.find((t) => t.function.name === name);
|
|
60
|
+
if (named) return named;
|
|
61
|
+
let best;
|
|
62
|
+
let bestScore = 0;
|
|
63
|
+
let tie = false;
|
|
64
|
+
for (const t of tools) {
|
|
65
|
+
const params = t.function.parameters;
|
|
66
|
+
const props = Object.keys(params.properties ?? {});
|
|
67
|
+
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
68
|
+
const score = keys.filter((k) => props.includes(k)).length;
|
|
69
|
+
if (score > bestScore) [best, bestScore, tie] = [t, score, false];
|
|
70
|
+
else if (score === bestScore && score > 0) tie = true;
|
|
71
|
+
}
|
|
72
|
+
return tie ? void 0 : best;
|
|
73
|
+
}
|
|
74
|
+
function recoverTextToolCalls(text, tools) {
|
|
75
|
+
if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
|
|
76
|
+
const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
|
|
77
|
+
const firstPair = text.search(PAIR);
|
|
78
|
+
const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
|
|
79
|
+
const bounds = starts.filter((i) => i >= cut);
|
|
80
|
+
if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
|
|
81
|
+
const toolCalls = [];
|
|
82
|
+
bounds.forEach((start, n) => {
|
|
83
|
+
const segment = text.slice(start, bounds[n + 1] ?? text.length);
|
|
84
|
+
const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
|
|
85
|
+
let args = [];
|
|
86
|
+
const flush = () => {
|
|
87
|
+
if (!args.length) return;
|
|
88
|
+
const keys = args.map(([k]) => k);
|
|
89
|
+
const tool = pickTool(name, keys, tools);
|
|
90
|
+
if (tool) {
|
|
91
|
+
const props = tool.function.parameters.properties ?? {};
|
|
92
|
+
const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
|
|
93
|
+
toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
|
|
94
|
+
}
|
|
95
|
+
args = [];
|
|
96
|
+
};
|
|
97
|
+
for (const m of segment.matchAll(PAIR)) {
|
|
98
|
+
if (args.some(([k]) => k === m[1])) flush();
|
|
99
|
+
args.push([m[1], m[2]]);
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
});
|
|
103
|
+
if (!toolCalls.length) return null;
|
|
104
|
+
return { text: text.slice(0, cut).trim(), toolCalls };
|
|
105
|
+
}
|
|
106
|
+
|
|
42
107
|
// src/tools/index.ts
|
|
43
108
|
import { z as z8 } from "zod";
|
|
44
109
|
|
|
@@ -279,7 +344,12 @@ var editTool = defineTool({
|
|
|
279
344
|
|
|
280
345
|
// src/tools/fetch.ts
|
|
281
346
|
import { z as z3 } from "zod";
|
|
282
|
-
var MAX_CHARS =
|
|
347
|
+
var MAX_CHARS = 12e3;
|
|
348
|
+
var FOCUS_CHUNKS = 3;
|
|
349
|
+
var FOCUS_SIZE = 800;
|
|
350
|
+
var CACHE_MAX = 16;
|
|
351
|
+
var CACHE_TTL_MS = 15 * 6e4;
|
|
352
|
+
var CACHE_MAX_CHARS = 2e6;
|
|
283
353
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
284
354
|
async function readCapped(res) {
|
|
285
355
|
const reader = res.body?.getReader();
|
|
@@ -315,45 +385,179 @@ function describeFetchError(error) {
|
|
|
315
385
|
}
|
|
316
386
|
return code ? `${detail} (${code})` : detail;
|
|
317
387
|
}
|
|
318
|
-
var
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
388
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
389
|
+
function isPrivateHost(host) {
|
|
390
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
391
|
+
if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
|
|
392
|
+
if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
|
|
393
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
|
|
394
|
+
if (v4) {
|
|
395
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
396
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
|
|
397
|
+
}
|
|
398
|
+
return !h.includes(".");
|
|
399
|
+
}
|
|
400
|
+
var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
|
|
401
|
+
function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
|
|
402
|
+
const chunks = [];
|
|
403
|
+
let cur = "";
|
|
404
|
+
for (const line of text.split("\n")) {
|
|
405
|
+
for (let i = 0; i < Math.max(line.length, 1); i += size) {
|
|
406
|
+
cur += `${line.slice(i, i + size)}
|
|
407
|
+
`;
|
|
408
|
+
if (cur.length >= size) {
|
|
409
|
+
chunks.push(cur.trimEnd());
|
|
410
|
+
cur = "";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (cur.trim()) chunks.push(cur.trimEnd());
|
|
415
|
+
const q = [...new Set(words(query))];
|
|
416
|
+
const docs = chunks.map(words);
|
|
417
|
+
const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
|
|
418
|
+
const idf = q.map((t) => {
|
|
419
|
+
const df = docs.filter((d) => d.includes(t)).length;
|
|
420
|
+
return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
421
|
+
});
|
|
422
|
+
const scored = docs.map((d, i) => ({
|
|
423
|
+
i,
|
|
424
|
+
score: q.reduce((sum, t, k) => {
|
|
425
|
+
const tf = d.filter((w) => w === t).length;
|
|
426
|
+
return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
|
|
427
|
+
}, 0)
|
|
428
|
+
})).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
|
|
429
|
+
return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
|
|
430
|
+
}
|
|
431
|
+
var cache = /* @__PURE__ */ new Map();
|
|
432
|
+
function remember(key, value, size) {
|
|
433
|
+
if (size > CACHE_MAX_CHARS) return;
|
|
434
|
+
cache.delete(key);
|
|
435
|
+
cache.set(key, value);
|
|
436
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
437
|
+
}
|
|
438
|
+
function recall(key) {
|
|
439
|
+
const hit = cache.get(key);
|
|
440
|
+
if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
|
|
441
|
+
cache.delete(key);
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
return hit;
|
|
445
|
+
}
|
|
446
|
+
var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
|
|
447
|
+
var withCost = (text, what) => `${what} \xB7 ~${tokensOf(text)} token`;
|
|
448
|
+
function createFetchTool(opts = {}) {
|
|
449
|
+
const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
|
|
450
|
+
const isPrivate = opts.isPrivate ?? isPrivateHost;
|
|
451
|
+
return defineTool({
|
|
452
|
+
name: "fetch",
|
|
453
|
+
description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
|
|
454
|
+
- Pass \`prompt\` (what you want to know from the page) to get only the relevant excerpt instead of the whole page. Prefer this whenever you are looking for specific facts.
|
|
455
|
+
- Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
|
|
456
|
+
- Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
|
|
457
|
+
schema: z3.object({
|
|
458
|
+
url: z3.string().describe("Absolute http(s) URL"),
|
|
459
|
+
prompt: z3.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
|
|
460
|
+
offset: z3.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
|
|
461
|
+
}),
|
|
462
|
+
kind: "fetch",
|
|
463
|
+
target: (input) => input.url,
|
|
464
|
+
async run(input, ctx) {
|
|
465
|
+
if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
|
|
466
|
+
const target = new URL(input.url);
|
|
467
|
+
const prompt = input.prompt?.trim();
|
|
468
|
+
const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
|
|
469
|
+
if (askKeenable) {
|
|
470
|
+
const key = `x
|
|
471
|
+
${input.url}
|
|
472
|
+
${prompt}`;
|
|
473
|
+
const hit = recall(key);
|
|
474
|
+
if (hit) return { output: hit, display: withCost(hit, "kutipan fokus \xB7 cache") };
|
|
475
|
+
const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
|
|
476
|
+
if (out) {
|
|
477
|
+
const output2 = `[Kutipan dari ${input.url} untuk "${prompt}" (diekstrak Keenable). Kalau jawabannya "tidak ada", coba fetch tanpa prompt.]
|
|
478
|
+
${out}`;
|
|
479
|
+
remember(key, output2, output2.length);
|
|
480
|
+
return { output: output2, display: withCost(output2, "kutipan fokus (Keenable)") };
|
|
340
481
|
}
|
|
482
|
+
}
|
|
483
|
+
let page = recall(`p
|
|
484
|
+
${input.url}`);
|
|
485
|
+
const cached = !!page;
|
|
486
|
+
if (!page) {
|
|
487
|
+
const loaded = await loadPage(input.url, ctx.signal);
|
|
488
|
+
if ("error" in loaded) return { output: loaded.error, isError: true };
|
|
489
|
+
page = loaded;
|
|
490
|
+
remember(`p
|
|
491
|
+
${input.url}`, page, page.text.length);
|
|
492
|
+
}
|
|
493
|
+
const { text, truncated } = page;
|
|
494
|
+
const via = cached ? " \xB7 cache" : "";
|
|
495
|
+
if (prompt && !input.offset) {
|
|
496
|
+
const picked = pickRelevant(text, prompt);
|
|
497
|
+
if (picked) {
|
|
498
|
+
const output2 = `[Kutipan paling relevan dari ${input.url} untuk "${prompt}" (${text.length} karakter total). Halaman penuh: fetch tanpa prompt.]
|
|
499
|
+
${picked}`;
|
|
500
|
+
return { output: output2, display: withCost(output2, `kutipan fokus (lokal)${via}`) };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const offset = input.offset ?? 0;
|
|
504
|
+
if (offset >= text.length && text.length > 0) {
|
|
505
|
+
return { output: `offset ${offset} melewati akhir halaman (${text.length} karakter).`, isError: true };
|
|
506
|
+
}
|
|
507
|
+
const end = Math.min(offset + MAX_CHARS, text.length);
|
|
508
|
+
let output = text.slice(offset, end);
|
|
509
|
+
if (end < text.length || truncated) {
|
|
510
|
+
const total = truncated ? `${text.length}+` : text.length;
|
|
511
|
+
output += `
|
|
512
|
+
\u2026 [dipotong: karakter ${offset}-${end} dari ${total}. Lanjut dengan offset=${end}, atau pakai prompt untuk kutipan terfokus.]`;
|
|
513
|
+
}
|
|
514
|
+
const size = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
515
|
+
return { output, display: withCost(output, `${size}${via}`) };
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
async function extractWithKeenable(endpoint, url, prompt, signal) {
|
|
520
|
+
try {
|
|
521
|
+
const u = new URL(endpoint);
|
|
522
|
+
u.searchParams.set("url", url);
|
|
523
|
+
u.searchParams.set("prompt", prompt);
|
|
524
|
+
u.searchParams.set("live", "true");
|
|
525
|
+
u.searchParams.set("max_chars", String(MAX_CHARS));
|
|
526
|
+
const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
|
|
527
|
+
if (!res.ok) return void 0;
|
|
528
|
+
const content = (await res.json()).content;
|
|
529
|
+
return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
|
|
530
|
+
} catch {
|
|
531
|
+
return void 0;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function loadPage(inputUrl, signal) {
|
|
535
|
+
let url = new URL(inputUrl);
|
|
536
|
+
let res;
|
|
537
|
+
try {
|
|
538
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
539
|
+
res = await fetch(url, { headers: { accept: ACCEPT }, signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
|
|
540
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
541
|
+
if (!location) break;
|
|
542
|
+
const next = new URL(location, url);
|
|
543
|
+
if (next.host !== url.host) {
|
|
341
544
|
await res.body?.cancel();
|
|
342
|
-
|
|
545
|
+
return { error: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).` };
|
|
343
546
|
}
|
|
344
|
-
|
|
345
|
-
|
|
547
|
+
await res.body?.cancel();
|
|
548
|
+
url = next;
|
|
346
549
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
|
|
350
|
-
if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${text.slice(0, 500)}`, isError: true };
|
|
351
|
-
const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
|
|
352
|
-
\u2026 [dipotong]` : text;
|
|
353
|
-
const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
354
|
-
return { output: clipped, display };
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return { error: `Gagal mengambil ${inputUrl}: ${describeFetchError(error)}` };
|
|
355
552
|
}
|
|
356
|
-
}
|
|
553
|
+
if (!res) return { error: `Gagal mengambil ${inputUrl}` };
|
|
554
|
+
const { text: body, truncated } = await readCapped(res);
|
|
555
|
+
const type = res.headers.get("content-type") ?? "";
|
|
556
|
+
const text = type.includes("html") ? htmlToText(body) : body;
|
|
557
|
+
if (!res.ok) return { error: `HTTP ${res.status} dari ${inputUrl}: ${text.slice(0, 500)}` };
|
|
558
|
+
return { text, truncated, at: Date.now() };
|
|
559
|
+
}
|
|
560
|
+
var fetchTool = createFetchTool();
|
|
357
561
|
|
|
358
562
|
// src/tools/glob.ts
|
|
359
563
|
import fg from "fast-glob";
|
|
@@ -536,70 +740,75 @@ function toolDefinitions(tools) {
|
|
|
536
740
|
});
|
|
537
741
|
}
|
|
538
742
|
|
|
539
|
-
// src/
|
|
540
|
-
var
|
|
541
|
-
var
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
743
|
+
// src/webBudget.ts
|
|
744
|
+
var KEEP_TOKENS = 4e4;
|
|
745
|
+
var PRUNE_MIN_TOKENS = 2e4;
|
|
746
|
+
var STUB = "[hasil tool lama dihapus:";
|
|
747
|
+
var tokens = (text) => Math.ceil(text.length / 4);
|
|
748
|
+
var isStub = (content) => content.startsWith(STUB);
|
|
749
|
+
function pruneOldFetches(messages) {
|
|
750
|
+
const urls = /* @__PURE__ */ new Map();
|
|
751
|
+
for (const m of messages) {
|
|
752
|
+
if (m.role !== "assistant") continue;
|
|
753
|
+
for (const c of m.tool_calls ?? []) {
|
|
754
|
+
if (c.function.name !== "fetch") continue;
|
|
755
|
+
try {
|
|
756
|
+
urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
|
|
757
|
+
} catch {
|
|
758
|
+
urls.set(c.id, "");
|
|
759
|
+
}
|
|
551
760
|
}
|
|
552
761
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
565
|
-
const score = keys.filter((k) => props.includes(k)).length;
|
|
566
|
-
if (score > bestScore) [best, bestScore, tie] = [t, score, false];
|
|
567
|
-
else if (score === bestScore && score > 0) tie = true;
|
|
762
|
+
let kept = 0;
|
|
763
|
+
let oldTokens = 0;
|
|
764
|
+
const old = [];
|
|
765
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
766
|
+
const m = messages[i];
|
|
767
|
+
if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
|
|
768
|
+
kept += tokens(m.content);
|
|
769
|
+
if (kept > KEEP_TOKENS) {
|
|
770
|
+
old.push(i);
|
|
771
|
+
oldTokens += tokens(m.content);
|
|
772
|
+
}
|
|
568
773
|
}
|
|
569
|
-
|
|
774
|
+
if (oldTokens <= PRUNE_MIN_TOKENS) return false;
|
|
775
|
+
for (const i of old) {
|
|
776
|
+
const m = messages[i];
|
|
777
|
+
messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Ambil lagi dengan fetch (pakai prompt untuk kutipan terfokus) kalau masih perlu.]` };
|
|
778
|
+
}
|
|
779
|
+
return true;
|
|
570
780
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
args = [];
|
|
593
|
-
};
|
|
594
|
-
for (const m of segment.matchAll(PAIR)) {
|
|
595
|
-
if (args.some(([k]) => k === m[1])) flush();
|
|
596
|
-
args.push([m[1], m[2]]);
|
|
781
|
+
var WebBudget = class {
|
|
782
|
+
fetches = 0;
|
|
783
|
+
chars = 0;
|
|
784
|
+
perTurn;
|
|
785
|
+
sessionChars;
|
|
786
|
+
constructor(limits = {}) {
|
|
787
|
+
this.perTurn = limits.perTurn ?? 8;
|
|
788
|
+
this.sessionChars = limits.sessionChars ?? 3e5;
|
|
789
|
+
}
|
|
790
|
+
startTurn() {
|
|
791
|
+
this.fetches = 0;
|
|
792
|
+
}
|
|
793
|
+
reset() {
|
|
794
|
+
this.fetches = 0;
|
|
795
|
+
this.chars = 0;
|
|
796
|
+
}
|
|
797
|
+
/** Reserves one fetch; returns the refusal to show the model when over budget. */
|
|
798
|
+
take() {
|
|
799
|
+
if (this.fetches >= this.perTurn) {
|
|
800
|
+
return `Batas ${this.perTurn} fetch per pertanyaan tercapai (tiap fetch memakai token user). Rangkum jawaban dari hasil yang sudah ada; kalau memang kurang, jelaskan ke user apa yang belum ketemu.`;
|
|
597
801
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
802
|
+
if (this.chars >= this.sessionChars) {
|
|
803
|
+
return `Batas total halaman web sesi ini tercapai (~${Math.round(this.sessionChars / 4e3)}k token). Rangkum dari hasil yang sudah ada dan tanyakan ke user apakah perlu mencari lebih banyak.`;
|
|
804
|
+
}
|
|
805
|
+
this.fetches++;
|
|
806
|
+
return void 0;
|
|
807
|
+
}
|
|
808
|
+
record(chars) {
|
|
809
|
+
this.chars += chars;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
603
812
|
|
|
604
813
|
// src/agent.ts
|
|
605
814
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
@@ -635,6 +844,7 @@ var Agent = class {
|
|
|
635
844
|
turnTools;
|
|
636
845
|
turnDefinitions;
|
|
637
846
|
readFiles = /* @__PURE__ */ new Set();
|
|
847
|
+
webBudget = new WebBudget();
|
|
638
848
|
maxSteps;
|
|
639
849
|
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
640
850
|
contextWindow;
|
|
@@ -664,11 +874,13 @@ var Agent = class {
|
|
|
664
874
|
load(messages) {
|
|
665
875
|
this.messages = messages;
|
|
666
876
|
this.readFiles.clear();
|
|
877
|
+
this.webBudget.reset();
|
|
667
878
|
this.lastInputTokens = 0;
|
|
668
879
|
}
|
|
669
880
|
clear() {
|
|
670
881
|
this.messages = [];
|
|
671
882
|
this.readFiles.clear();
|
|
883
|
+
this.webBudget.reset();
|
|
672
884
|
this.lastInputTokens = 0;
|
|
673
885
|
this.opts.onReset?.();
|
|
674
886
|
}
|
|
@@ -690,6 +902,7 @@ ${completion.text}` });
|
|
|
690
902
|
this.turnTools = this.tools;
|
|
691
903
|
this.turnDefinitions = this.definitions;
|
|
692
904
|
this.opts.onTurnStart?.();
|
|
905
|
+
this.webBudget.startTurn();
|
|
693
906
|
try {
|
|
694
907
|
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
695
908
|
this.push({ role: "user", content: text });
|
|
@@ -698,6 +911,7 @@ ${completion.text}` });
|
|
|
698
911
|
await this.compact(signal);
|
|
699
912
|
this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
|
|
700
913
|
}
|
|
914
|
+
pruneOldFetches(this.messages);
|
|
701
915
|
let completion = await this.provider.chat({
|
|
702
916
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
|
|
703
917
|
tools: this.turnDefinitions,
|
|
@@ -801,6 +1015,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
801
1015
|
}
|
|
802
1016
|
const input = parsed.data;
|
|
803
1017
|
const target = tool.target(input);
|
|
1018
|
+
const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
|
|
1019
|
+
if (overBudget) return fail(overBudget, target);
|
|
804
1020
|
const ctx = {
|
|
805
1021
|
cwd: this.opts.cwd,
|
|
806
1022
|
signal,
|
|
@@ -846,6 +1062,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
846
1062
|
} catch (error) {
|
|
847
1063
|
result = { output: `Error: ${error.message}`, isError: true };
|
|
848
1064
|
}
|
|
1065
|
+
if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
|
|
849
1066
|
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
|
|
850
1067
|
return result.output;
|
|
851
1068
|
}
|
|
@@ -1180,8 +1397,8 @@ ${def.prompt}`,
|
|
|
1180
1397
|
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1181
1398
|
await child.run(input.prompt, ctx.signal);
|
|
1182
1399
|
ctx.addUsage?.(child.totalUsage);
|
|
1183
|
-
const
|
|
1184
|
-
const display = `${steps} langkah \xB7 ${
|
|
1400
|
+
const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1401
|
+
const display = `${steps} langkah \xB7 ${tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2} token`;
|
|
1185
1402
|
if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
|
|
1186
1403
|
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
|
|
1187
1404
|
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
@@ -1374,7 +1591,7 @@ function createRuntime(opts) {
|
|
|
1374
1591
|
}
|
|
1375
1592
|
|
|
1376
1593
|
// src/version.ts
|
|
1377
|
-
var VERSION = true ? "0.4.0-beta.
|
|
1594
|
+
var VERSION = true ? "0.4.0-beta.13" : "0.0.0-dev";
|
|
1378
1595
|
|
|
1379
1596
|
// src/cli.ts
|
|
1380
1597
|
async function main() {
|
|
@@ -1400,7 +1617,7 @@ async function main() {
|
|
|
1400
1617
|
return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1401
1618
|
}
|
|
1402
1619
|
if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
|
|
1403
|
-
const { runIntegrationCommand } = await import("./integrationCli-
|
|
1620
|
+
const { runIntegrationCommand } = await import("./integrationCli-3CGHHTM2.js");
|
|
1404
1621
|
return runIntegrationCommand(args, {
|
|
1405
1622
|
env: process.env,
|
|
1406
1623
|
cwd,
|
|
@@ -1436,7 +1653,7 @@ async function main() {
|
|
|
1436
1653
|
}
|
|
1437
1654
|
}
|
|
1438
1655
|
if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
|
|
1439
|
-
const { startInteractive } = await import("./ui-
|
|
1656
|
+
const { startInteractive } = await import("./ui-JD5YWGPE.js");
|
|
1440
1657
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1441
1658
|
}
|
|
1442
1659
|
main().then(
|
|
@@ -305,6 +305,8 @@ ${managed}
|
|
|
305
305
|
deps.out(`DeepSeek Harness terhubung ke BotConnector (${models.length} model).`);
|
|
306
306
|
deps.out(`Config: ${settingsPath}`);
|
|
307
307
|
deps.out("Provider: botconnector (OpenAI Chat Completions)");
|
|
308
|
+
deps.out("Jalankan: dsh web");
|
|
309
|
+
deps.out("Bentuk panjang yang setara: dsh --profile web");
|
|
308
310
|
}
|
|
309
311
|
function managedProfilePath(target, env, ext = "env") {
|
|
310
312
|
return join(integrationHome(env), `${target}.${ext}`);
|
|
@@ -248,6 +248,11 @@ function Markdown({ text }) {
|
|
|
248
248
|
// src/ui/LinePrompt.tsx
|
|
249
249
|
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
250
250
|
import { useState } from "react";
|
|
251
|
+
|
|
252
|
+
// src/ui/focusReport.ts
|
|
253
|
+
var isFocusReport = (input) => input === "[I" || input === "[O";
|
|
254
|
+
|
|
255
|
+
// src/ui/LinePrompt.tsx
|
|
251
256
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
252
257
|
function LinePrompt({ label, mask, onSubmit, onCancel }) {
|
|
253
258
|
const [value, setValue] = useState("");
|
|
@@ -255,7 +260,7 @@ function LinePrompt({ label, mask, onSubmit, onCancel }) {
|
|
|
255
260
|
if (key.return) onSubmit(value.trim());
|
|
256
261
|
else if (key.escape) onCancel();
|
|
257
262
|
else if (key.backspace || key.delete) setValue((v) => v.slice(0, -1));
|
|
258
|
-
else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow) setValue((v) => v + input.replace(/[\r\n]/g, ""));
|
|
263
|
+
else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow && !isFocusReport(input)) setValue((v) => v + input.replace(/[\r\n]/g, ""));
|
|
259
264
|
});
|
|
260
265
|
const shown = mask ? "\u2022".repeat(value.length) : value;
|
|
261
266
|
return /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: /* @__PURE__ */ jsxs(Text2, { children: [
|
|
@@ -284,7 +289,7 @@ function ListPicker({ title, items, onPick }) {
|
|
|
284
289
|
else if (key.backspace || key.delete) {
|
|
285
290
|
setFilter((f) => f.slice(0, -1));
|
|
286
291
|
setIndex(0);
|
|
287
|
-
} else if (!key.ctrl && !key.meta && !key.tab && input) {
|
|
292
|
+
} else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
|
|
288
293
|
setFilter((f) => f + input);
|
|
289
294
|
setIndex(0);
|
|
290
295
|
}
|
|
@@ -329,7 +334,7 @@ function ModelPicker({ groups, current, onPick }) {
|
|
|
329
334
|
else if (key.backspace || key.delete) {
|
|
330
335
|
setFilter((f) => f.slice(0, -1));
|
|
331
336
|
setIndex(0);
|
|
332
|
-
} else if (!key.ctrl && !key.meta && !key.tab && input) {
|
|
337
|
+
} else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
|
|
333
338
|
setFilter((f) => f + input);
|
|
334
339
|
setIndex(0);
|
|
335
340
|
}
|
|
@@ -498,7 +503,7 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
|
|
|
498
503
|
edit((v) => completeFile(v, cwd));
|
|
499
504
|
return;
|
|
500
505
|
}
|
|
501
|
-
if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow) return;
|
|
506
|
+
if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow || isFocusReport(input)) return;
|
|
502
507
|
edit((v) => v + input.replace(/\r/g, "\n"));
|
|
503
508
|
},
|
|
504
509
|
{ isActive: !disabled }
|