@botconnector/bccli 0.4.0-beta.37 → 0.4.0-beta.39
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 +8 -0
- package/dist/acp-4XE2KOIJ.js +114 -0
- package/dist/{args-MNVI2OJI.js → args-BD2PHEKG.js} +4 -4
- package/dist/{chunk-TASYQW7B.js → chunk-AVB6M4JE.js} +2 -2
- package/dist/{chunk-FCQCZCKO.js → chunk-EONFDWBE.js} +2 -2
- package/dist/{chunk-7TABTC32.js → chunk-GGNXHJL6.js} +4 -3
- package/dist/chunk-HE33XAPZ.js +341 -0
- package/dist/{chunk-HPZSXX4W.js → chunk-ISCT3PN3.js} +2 -2
- package/dist/{chunk-BB4CVWHS.js → chunk-JROUQ5YS.js} +6 -45
- package/dist/chunk-LN32JIZR.js +56 -0
- package/dist/{chunk-4RMP4CNT.js → chunk-M23QBX3V.js} +52 -12
- package/dist/chunk-M7LLJQ7M.js +310 -0
- package/dist/{chunk-YURAIW7A.js → chunk-NI5JZUZ3.js} +1 -1
- package/dist/chunk-PDZPPSLU.js +8 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-QETGQXPL.js +2125 -0
- package/dist/chunk-RV74TCEP.js +231 -0
- package/dist/{chunk-U66JJ6RN.js → chunk-SESBH44L.js} +1 -1
- package/dist/{chunk-3JRBXJVB.js → chunk-VM7HYAJS.js} +1 -1
- package/dist/{chunk-ASTNW3X2.js → chunk-ZG7HCXYA.js} +89 -9
- package/dist/{chunk-HXSREA7S.js → chunk-ZMU7MXCH.js} +184 -295
- package/dist/cli.js +65 -2097
- package/dist/{config-JRO5EOKZ.js → config-C4KLLDC6.js} +3 -3
- package/dist/{integrationCli-ZTZTQD2I.js → integrationCli-HR7CDUSI.js} +2 -2
- package/dist/{login-MHVCON5R.js → login-IWBRUNT4.js} +1 -1
- package/dist/{mcpCli-5ZSAMBJS.js → mcpCli-5QTXH7XJ.js} +9 -5
- package/dist/modelsCli-DKTT3UZD.js +36 -0
- package/dist/providerCli-DDH4ZZUY.js +15 -0
- package/dist/sdk.js +5114 -0
- package/dist/serve-7BP5BVX3.js +514 -0
- package/dist/serve-I2VQUTHR.js +178 -0
- package/dist/session-H2YL5RSB.js +12 -0
- package/dist/setup-FGCFKC7Z.js +23 -0
- package/dist/{ui-YLIKFLGN.js → ui-MLOXPAHT.js} +292 -54
- package/dist/update-73WI27VM.js +109 -0
- package/package.json +5 -2
- package/dist/chunk-H5N4ST6B.js +0 -149
- package/dist/providerCli-Z3TZ5XSZ.js +0 -14
- package/dist/session-IYVTCUBC.js +0 -12
package/README.md
CHANGED
|
@@ -160,6 +160,14 @@ seperti Claude Code). Semua alat MCP selalu minta izin.
|
|
|
160
160
|
|
|
161
161
|
`.bccli/mcp.json` di dalam repo tidak dijalankan sebelum kamu setujui (ditanya sekali per folder).
|
|
162
162
|
|
|
163
|
+
`bccli mcp serve` menjalankan MCP server stdio dengan tools BotConnector milikmu
|
|
164
|
+
(`bc_search`, `bc_fetch`, `bc_models`, `bc_chat`) untuk dipakai Claude Code, Cursor,
|
|
165
|
+
opencode, dan host MCP lain. Butuh `bccli login` (atau `BOTCONNECTOR_API_KEY`):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
claude mcp add botconnector -- npx -y @botconnector/bccli mcp serve
|
|
169
|
+
```
|
|
170
|
+
|
|
163
171
|
## Subagent
|
|
164
172
|
|
|
165
173
|
Agent bisa mendelegasikan pekerjaan lewat alat `task`:
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/acp.ts
|
|
4
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
5
|
+
function runAcp(input, output, opts) {
|
|
6
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
7
|
+
let nextSession = 1;
|
|
8
|
+
const write = (msg) => {
|
|
9
|
+
output.write(`${JSON.stringify(msg)}
|
|
10
|
+
`);
|
|
11
|
+
};
|
|
12
|
+
const reply = (id, result) => write({ jsonrpc: "2.0", id, result });
|
|
13
|
+
const replyError = (id, code, message) => write({ jsonrpc: "2.0", id, error: { code, message } });
|
|
14
|
+
const handle = async (msg) => {
|
|
15
|
+
const { id, method, params } = msg ?? {};
|
|
16
|
+
if (method === "initialize") {
|
|
17
|
+
return reply(id, {
|
|
18
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
19
|
+
agentCapabilities: {
|
|
20
|
+
loadSession: false,
|
|
21
|
+
promptCapabilities: { image: false, audio: false, embeddedContext: false }
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (method === "session/new") {
|
|
26
|
+
const sessionId = `s${nextSession++}`;
|
|
27
|
+
sessions.set(sessionId, { controller: null });
|
|
28
|
+
return reply(id, { sessionId });
|
|
29
|
+
}
|
|
30
|
+
if (method === "session/cancel") {
|
|
31
|
+
sessions.get(params?.sessionId ?? "")?.controller?.abort();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (method === "session/prompt") {
|
|
35
|
+
const sid = params?.sessionId ?? "";
|
|
36
|
+
const session = sessions.get(sid);
|
|
37
|
+
if (!session) return replyError(id, -32602, "Unknown session");
|
|
38
|
+
const text = (params?.prompt ?? []).filter((p) => p?.type === "text").map((p) => String(p.text ?? "")).join("\n");
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
session.controller = controller;
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
controller.signal.addEventListener("abort", () => {
|
|
43
|
+
cancelled = true;
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
const agent = opts.newAgent(params?.cwd ?? opts.cwd);
|
|
47
|
+
await agent.run(
|
|
48
|
+
text,
|
|
49
|
+
(chunk) => {
|
|
50
|
+
write({
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
method: "session/update",
|
|
53
|
+
params: {
|
|
54
|
+
sessionId: sid,
|
|
55
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: chunk } }
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
controller.signal
|
|
60
|
+
);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return replyError(id, -32603, e.message);
|
|
63
|
+
} finally {
|
|
64
|
+
session.controller = null;
|
|
65
|
+
}
|
|
66
|
+
return reply(id, { stopReason: cancelled ? "cancelled" : "end_turn" });
|
|
67
|
+
}
|
|
68
|
+
if (id !== void 0) return replyError(id, -32601, "Method not found");
|
|
69
|
+
};
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
let buf = "";
|
|
72
|
+
const onData = (chunk) => {
|
|
73
|
+
buf += chunk.toString();
|
|
74
|
+
let idx = buf.indexOf("\n");
|
|
75
|
+
while (idx >= 0) {
|
|
76
|
+
const line = buf.slice(0, idx);
|
|
77
|
+
buf = buf.slice(idx + 1);
|
|
78
|
+
idx = buf.indexOf("\n");
|
|
79
|
+
if (!line.trim()) continue;
|
|
80
|
+
let msg;
|
|
81
|
+
try {
|
|
82
|
+
msg = JSON.parse(line);
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
void handle(msg);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const onEnd = () => {
|
|
90
|
+
input.removeListener("data", onData);
|
|
91
|
+
resolve();
|
|
92
|
+
};
|
|
93
|
+
input.on("data", onData);
|
|
94
|
+
input.on("end", onEnd);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function runtimeAgent(cwd) {
|
|
98
|
+
return {
|
|
99
|
+
async run(text, onChunk, signal) {
|
|
100
|
+
const { parseCliArgs } = await import("./args-BD2PHEKG.js");
|
|
101
|
+
const { createRuntime } = await import("./setup-FGCFKC7Z.js");
|
|
102
|
+
const rt = createRuntime({ cwd, args: { ...parseCliArgs([]), allowAll: true } });
|
|
103
|
+
rt.agent.onEvent = (event) => {
|
|
104
|
+
if (event.type === "text") onChunk(event.delta);
|
|
105
|
+
};
|
|
106
|
+
await rt.agent.run(text, signal);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
ACP_PROTOCOL_VERSION,
|
|
112
|
+
runAcp,
|
|
113
|
+
runtimeAgent
|
|
114
|
+
};
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
helpText,
|
|
4
4
|
parseCliArgs
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ZG7HCXYA.js";
|
|
6
6
|
import "./chunk-R3ND76LW.js";
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-NI5JZUZ3.js";
|
|
8
|
+
import "./chunk-M7LLJQ7M.js";
|
|
9
|
+
import "./chunk-M23QBX3V.js";
|
|
10
10
|
export {
|
|
11
11
|
helpText,
|
|
12
12
|
parseCliArgs
|
|
@@ -35,8 +35,8 @@ function readSecret(prompt) {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
async function runLogin(providerId, cwd) {
|
|
38
|
-
const { runProviderCommand } = await import("./providerCli-
|
|
39
|
-
const { parseCliArgs } = await import("./args-
|
|
38
|
+
const { runProviderCommand } = await import("./providerCli-DDH4ZZUY.js");
|
|
39
|
+
const { parseCliArgs } = await import("./args-BD2PHEKG.js");
|
|
40
40
|
return runProviderCommand(parseCliArgs(["provider", "add", providerId]), {
|
|
41
41
|
env: process.env,
|
|
42
42
|
cwd,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
reasoningPayload
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NI5JZUZ3.js";
|
|
5
5
|
import {
|
|
6
6
|
t
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-M23QBX3V.js";
|
|
8
8
|
|
|
9
9
|
// src/provider.ts
|
|
10
10
|
import OpenAI from "openai";
|
|
@@ -869,7 +869,8 @@ function createProvider(options) {
|
|
|
869
869
|
const res = await doFetch(`${baseURL}/models`, { headers });
|
|
870
870
|
if (!res.ok) throw new ProviderError(t("{status} while fetching the model list", { status: res.status }), res.status);
|
|
871
871
|
const body = await res.json();
|
|
872
|
-
|
|
872
|
+
const ids = (body.data ?? []).map((m) => m?.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
873
|
+
return [...new Set(ids)].sort();
|
|
873
874
|
}
|
|
874
875
|
};
|
|
875
876
|
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
defineTool
|
|
4
|
+
} from "./chunk-PSO7R5AD.js";
|
|
5
|
+
import {
|
|
6
|
+
t
|
|
7
|
+
} from "./chunk-M23QBX3V.js";
|
|
8
|
+
|
|
9
|
+
// src/tools/websearch.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
|
|
12
|
+
title: String(r.title || r.url).slice(0, 200),
|
|
13
|
+
url: r.url,
|
|
14
|
+
snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
|
|
15
|
+
}));
|
|
16
|
+
async function postJson(url, body, headers, signal) {
|
|
17
|
+
const res = await fetch(url, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "content-type": "application/json", ...headers },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
function createWebSearchTool(opts) {
|
|
27
|
+
const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
|
|
28
|
+
return defineTool({
|
|
29
|
+
name: "web_search",
|
|
30
|
+
description: "Search the web. Returns titles, URLs and short snippets. Use it to find the right pages instead of guessing URLs; then fetch a result URL when you need the full page.",
|
|
31
|
+
schema: z.object({
|
|
32
|
+
query: z.string().min(1).max(500).describe("Search query"),
|
|
33
|
+
max_results: z.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
|
|
34
|
+
}),
|
|
35
|
+
kind: "read",
|
|
36
|
+
target: (input) => input.query,
|
|
37
|
+
parallelSafe: () => true,
|
|
38
|
+
async run(input, ctx) {
|
|
39
|
+
const max = input.max_results ?? 6;
|
|
40
|
+
const errors = [];
|
|
41
|
+
let results = [];
|
|
42
|
+
const bc = opts.botconnector();
|
|
43
|
+
if (bc) {
|
|
44
|
+
try {
|
|
45
|
+
const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
|
|
46
|
+
results = clean(data.results, max);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
errors.push(`BotConnector: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!results.length) {
|
|
52
|
+
try {
|
|
53
|
+
const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
|
|
54
|
+
results = clean(data.results, max);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
errors.push(`Keenable: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!results.length) {
|
|
60
|
+
return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
|
|
61
|
+
}
|
|
62
|
+
const output = results.map((r, i) => `${i + 1}. ${r.title}
|
|
63
|
+
${r.url}${r.snippet ? `
|
|
64
|
+
${r.snippet}` : ""}`).join("\n");
|
|
65
|
+
return { output, display: t("{n} results", { n: results.length }) };
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/tools/fetch.ts
|
|
71
|
+
import { lookup as dnsCbLookup } from "dns";
|
|
72
|
+
import { lookup as dnsLookup } from "dns/promises";
|
|
73
|
+
import { isIP } from "net";
|
|
74
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
75
|
+
import { z as z2 } from "zod";
|
|
76
|
+
var MAX_CHARS = 12e3;
|
|
77
|
+
var FOCUS_CHUNKS = 3;
|
|
78
|
+
var FOCUS_SIZE = 800;
|
|
79
|
+
var CACHE_MAX = 16;
|
|
80
|
+
var CACHE_TTL_MS = 15 * 6e4;
|
|
81
|
+
var CACHE_MAX_CHARS = 2e6;
|
|
82
|
+
var MAX_BYTES = 5 * 1024 * 1024;
|
|
83
|
+
async function readCapped(res) {
|
|
84
|
+
const reader = res.body?.getReader();
|
|
85
|
+
if (!reader) return { text: "", truncated: false };
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let size = 0;
|
|
88
|
+
for (; ; ) {
|
|
89
|
+
const { done, value } = await reader.read();
|
|
90
|
+
if (done) return { text: Buffer.concat(chunks).toString("utf8"), truncated: false };
|
|
91
|
+
size += value.length;
|
|
92
|
+
if (size > MAX_BYTES) {
|
|
93
|
+
chunks.push(value.subarray(0, value.length - (size - MAX_BYTES)));
|
|
94
|
+
await reader.cancel();
|
|
95
|
+
return { text: Buffer.concat(chunks).toString("utf8"), truncated: true };
|
|
96
|
+
}
|
|
97
|
+
chunks.push(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function htmlToText(html) {
|
|
101
|
+
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");
|
|
102
|
+
}
|
|
103
|
+
function describeFetchError(error) {
|
|
104
|
+
const cause = error.cause;
|
|
105
|
+
const code = cause?.code ?? "";
|
|
106
|
+
const detail = cause?.message ?? error.message;
|
|
107
|
+
if (error.name === "TimeoutError") return "no response within 30 seconds";
|
|
108
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS could not find the host (typo, offline, or blocked by DNS/ISP)`;
|
|
109
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
|
|
110
|
+
return `${detail} \u2014 TLS certificate rejected; often caused by an antivirus or proxy that inspects HTTPS. Try running with NODE_OPTIONS=--use-system-ca`;
|
|
111
|
+
}
|
|
112
|
+
if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
|
|
113
|
+
return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
|
|
114
|
+
}
|
|
115
|
+
return code ? `${detail} (${code})` : detail;
|
|
116
|
+
}
|
|
117
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
118
|
+
function isPrivateHost(host) {
|
|
119
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
120
|
+
if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
|
|
121
|
+
if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
|
|
122
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
|
|
123
|
+
if (v4) {
|
|
124
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
125
|
+
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;
|
|
126
|
+
}
|
|
127
|
+
return !h.includes(".");
|
|
128
|
+
}
|
|
129
|
+
var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
|
|
130
|
+
function pinLookup(pinned) {
|
|
131
|
+
return (hostname, options, callback) => {
|
|
132
|
+
const opts = typeof options === "object" && options !== null ? options : {};
|
|
133
|
+
const family = typeof options === "number" ? options : opts.family;
|
|
134
|
+
const realDns = () => dnsCbLookup(hostname, typeof options === "number" ? { family: options } : opts, callback);
|
|
135
|
+
if (!pinned.length) return realDns();
|
|
136
|
+
const list = family ? pinned.filter((a) => a.family === family) : pinned;
|
|
137
|
+
if (!list.length) {
|
|
138
|
+
callback(Object.assign(new Error(`no pinned address for family ${family}`), { code: "ENOTFOUND" }), "");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (opts.all) callback(null, list);
|
|
142
|
+
else callback(null, list[0].address, list[0].family);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
|
|
146
|
+
const chunks = [];
|
|
147
|
+
let cur = "";
|
|
148
|
+
for (const line of text.split("\n")) {
|
|
149
|
+
for (let i = 0; i < Math.max(line.length, 1); i += size) {
|
|
150
|
+
cur += `${line.slice(i, i + size)}
|
|
151
|
+
`;
|
|
152
|
+
if (cur.length >= size) {
|
|
153
|
+
chunks.push(cur.trimEnd());
|
|
154
|
+
cur = "";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (cur.trim()) chunks.push(cur.trimEnd());
|
|
159
|
+
const q = [...new Set(words(query))];
|
|
160
|
+
const docs = chunks.map(words);
|
|
161
|
+
const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
|
|
162
|
+
const idf = q.map((t2) => {
|
|
163
|
+
const df = docs.filter((d) => d.includes(t2)).length;
|
|
164
|
+
return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
165
|
+
});
|
|
166
|
+
const scored = docs.map((d, i) => ({
|
|
167
|
+
i,
|
|
168
|
+
score: q.reduce((sum, t2, k) => {
|
|
169
|
+
const tf = d.filter((w) => w === t2).length;
|
|
170
|
+
return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
|
|
171
|
+
}, 0)
|
|
172
|
+
})).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
|
|
173
|
+
return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
|
|
174
|
+
}
|
|
175
|
+
var cache = /* @__PURE__ */ new Map();
|
|
176
|
+
function remember(key, value, size) {
|
|
177
|
+
if (size > CACHE_MAX_CHARS) return;
|
|
178
|
+
cache.delete(key);
|
|
179
|
+
cache.set(key, value);
|
|
180
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
181
|
+
}
|
|
182
|
+
function recall(key) {
|
|
183
|
+
const hit = cache.get(key);
|
|
184
|
+
if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
|
|
185
|
+
cache.delete(key);
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
return hit;
|
|
189
|
+
}
|
|
190
|
+
var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
|
|
191
|
+
var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
|
|
192
|
+
function createFetchTool(opts = {}) {
|
|
193
|
+
const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
|
|
194
|
+
const isPrivate = opts.isPrivate ?? isPrivateHost;
|
|
195
|
+
const lookup = opts.lookup ?? ((hostname) => dnsLookup(hostname, { all: true }));
|
|
196
|
+
return defineTool({
|
|
197
|
+
name: "fetch",
|
|
198
|
+
description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
|
|
199
|
+
- 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.
|
|
200
|
+
- Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
|
|
201
|
+
- Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
|
|
202
|
+
schema: z2.object({
|
|
203
|
+
url: z2.string().describe("Absolute http(s) URL"),
|
|
204
|
+
prompt: z2.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
|
|
205
|
+
offset: z2.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
|
|
206
|
+
}),
|
|
207
|
+
kind: "fetch",
|
|
208
|
+
target: (input) => input.url,
|
|
209
|
+
async run(input, ctx) {
|
|
210
|
+
if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
|
|
211
|
+
const target = new URL(input.url);
|
|
212
|
+
let pinned;
|
|
213
|
+
if (!isPrivate(target.hostname) && isIP(target.hostname) === 0) {
|
|
214
|
+
try {
|
|
215
|
+
const addresses = await lookup(target.hostname);
|
|
216
|
+
const internal = addresses.find((a) => isPrivate(a.address));
|
|
217
|
+
if (internal) {
|
|
218
|
+
return {
|
|
219
|
+
output: t("Blocked: {host} resolves to a private address ({addr}). Fetching an internal host through a public DNS name is not allowed.", { host: target.hostname, addr: internal.address }),
|
|
220
|
+
isError: true
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
pinned = addresses;
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const prompt = input.prompt?.trim();
|
|
228
|
+
const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
|
|
229
|
+
if (askKeenable) {
|
|
230
|
+
const key = `x
|
|
231
|
+
${input.url}
|
|
232
|
+
${prompt}`;
|
|
233
|
+
const hit = recall(key);
|
|
234
|
+
if (hit) {
|
|
235
|
+
ctx.refundFetch?.();
|
|
236
|
+
return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
|
|
237
|
+
}
|
|
238
|
+
const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
|
|
239
|
+
if (out) {
|
|
240
|
+
const output2 = `[Excerpt from ${input.url} for "${prompt}" (extracted by Keenable). If it says the answer is not there, try fetch without a prompt.]
|
|
241
|
+
${out}`;
|
|
242
|
+
remember(key, output2, output2.length);
|
|
243
|
+
return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
let page = recall(`p
|
|
247
|
+
${input.url}`);
|
|
248
|
+
const cached = !!page;
|
|
249
|
+
if (cached) ctx.refundFetch?.();
|
|
250
|
+
if (!page) {
|
|
251
|
+
const loaded = await loadPage(input.url, ctx.signal, pinned, opts.fetchPage);
|
|
252
|
+
if ("error" in loaded) return { output: loaded.error, isError: true };
|
|
253
|
+
page = loaded;
|
|
254
|
+
remember(`p
|
|
255
|
+
${input.url}`, page, page.text.length);
|
|
256
|
+
}
|
|
257
|
+
const { text, truncated } = page;
|
|
258
|
+
const via = cached ? " \xB7 cache" : "";
|
|
259
|
+
if (prompt && !input.offset) {
|
|
260
|
+
const picked = pickRelevant(text, prompt);
|
|
261
|
+
if (picked) {
|
|
262
|
+
const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
|
|
263
|
+
${picked}`;
|
|
264
|
+
return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const offset = input.offset ?? 0;
|
|
268
|
+
if (offset >= text.length && text.length > 0) {
|
|
269
|
+
return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
|
|
270
|
+
}
|
|
271
|
+
const end = Math.min(offset + MAX_CHARS, text.length);
|
|
272
|
+
let output = text.slice(offset, end);
|
|
273
|
+
if (end < text.length || truncated) {
|
|
274
|
+
const total = truncated ? `${text.length}+` : text.length;
|
|
275
|
+
output += `
|
|
276
|
+
\u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
|
|
277
|
+
}
|
|
278
|
+
const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
|
|
279
|
+
return { output, display: withCost(output, `${size}${via}`) };
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function extractWithKeenable(endpoint, url, prompt, signal) {
|
|
284
|
+
try {
|
|
285
|
+
const u = new URL(endpoint);
|
|
286
|
+
u.searchParams.set("url", url);
|
|
287
|
+
u.searchParams.set("prompt", prompt);
|
|
288
|
+
u.searchParams.set("live", "true");
|
|
289
|
+
u.searchParams.set("max_chars", String(MAX_CHARS));
|
|
290
|
+
const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
|
|
291
|
+
if (!res.ok) return void 0;
|
|
292
|
+
const content = (await res.json()).content;
|
|
293
|
+
return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
|
|
294
|
+
} catch {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function loadPage(inputUrl, signal, pinned, fetchPage) {
|
|
299
|
+
let url = new URL(inputUrl);
|
|
300
|
+
const dispatcher = pinned?.length ? new Agent({ connect: { lookup: pinLookup(pinned) } }) : void 0;
|
|
301
|
+
const doFetch = fetchPage ?? (dispatcher ? undiciFetch : fetch);
|
|
302
|
+
try {
|
|
303
|
+
let res;
|
|
304
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
305
|
+
res = await doFetch(url, {
|
|
306
|
+
headers: { accept: ACCEPT },
|
|
307
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]),
|
|
308
|
+
redirect: "manual",
|
|
309
|
+
...dispatcher ? { dispatcher } : {}
|
|
310
|
+
});
|
|
311
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
312
|
+
if (!location) break;
|
|
313
|
+
const next = new URL(location, url);
|
|
314
|
+
if (next.host !== url.host) {
|
|
315
|
+
await res.body?.cancel();
|
|
316
|
+
return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
|
|
317
|
+
}
|
|
318
|
+
await res.body?.cancel();
|
|
319
|
+
url = next;
|
|
320
|
+
}
|
|
321
|
+
if (!res) return { error: `Failed to fetch ${inputUrl}` };
|
|
322
|
+
const { text: body, truncated } = await readCapped(res);
|
|
323
|
+
const type = res.headers.get("content-type") ?? "";
|
|
324
|
+
const text = type.includes("html") ? htmlToText(body) : body;
|
|
325
|
+
if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
|
|
326
|
+
return { text, truncated, at: Date.now() };
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
|
|
329
|
+
} finally {
|
|
330
|
+
await dispatcher?.close().catch(() => {
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
var fetchTool = createFetchTool();
|
|
335
|
+
|
|
336
|
+
export {
|
|
337
|
+
isPrivateHost,
|
|
338
|
+
createFetchTool,
|
|
339
|
+
fetchTool,
|
|
340
|
+
createWebSearchTool
|
|
341
|
+
};
|
|
@@ -1,59 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
listAllModels
|
|
4
|
+
} from "./chunk-LN32JIZR.js";
|
|
2
5
|
import {
|
|
3
6
|
hasKey,
|
|
4
7
|
providerName,
|
|
5
8
|
removeGlobalProvider,
|
|
6
9
|
writeGlobalConfig
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import {
|
|
9
|
-
createProvider
|
|
10
|
-
} from "./chunk-7TABTC32.js";
|
|
10
|
+
} from "./chunk-SESBH44L.js";
|
|
11
11
|
import {
|
|
12
12
|
PRESETS,
|
|
13
13
|
loadConfig,
|
|
14
|
-
readCredentials,
|
|
15
14
|
saveCredential
|
|
16
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-M7LLJQ7M.js";
|
|
17
16
|
import {
|
|
18
17
|
t
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
|
|
21
|
-
// src/models.ts
|
|
22
|
-
function order(ids) {
|
|
23
|
-
const presetIds = PRESETS.map((p) => p.id);
|
|
24
|
-
const presets = presetIds.filter((id) => ids.includes(id));
|
|
25
|
-
const custom = ids.filter((id) => !presetIds.includes(id)).sort();
|
|
26
|
-
return [...presets, ...custom];
|
|
27
|
-
}
|
|
28
|
-
async function listAllModels(config, env = process.env, opts = {}) {
|
|
29
|
-
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
30
|
-
const baseFetch = opts.fetch ?? fetch;
|
|
31
|
-
const untrusted = new Set(config.projectProviders ?? []);
|
|
32
|
-
const ids = order(Object.keys(config.providers)).filter((id) => opts.only ? id === opts.only : !untrusted.has(id) && hasKey(config, id, env));
|
|
33
|
-
const results = await Promise.all(
|
|
34
|
-
ids.map(async (id) => {
|
|
35
|
-
const provider = config.providers[id];
|
|
36
|
-
const apiKey = (opts.keyOverride && id === opts.only ? opts.keyOverride : void 0) || (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[id] || void 0;
|
|
37
|
-
const local = !provider.apiKeyEnv;
|
|
38
|
-
const timedFetch = ((input, init) => baseFetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
39
|
-
const list = createProvider({ baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey, model: "", fetch: timedFetch }).listModels();
|
|
40
|
-
let timer;
|
|
41
|
-
const timeout = new Promise((_, reject) => {
|
|
42
|
-
timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
|
43
|
-
});
|
|
44
|
-
try {
|
|
45
|
-
const models = await Promise.race([list, timeout]);
|
|
46
|
-
return { providerId: id, providerName: providerName(config, id), models };
|
|
47
|
-
} catch {
|
|
48
|
-
if (local && !opts.only) return void 0;
|
|
49
|
-
return { providerId: id, providerName: providerName(config, id), models: [], error: t("could not be reached") };
|
|
50
|
-
} finally {
|
|
51
|
-
clearTimeout(timer);
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
);
|
|
55
|
-
return results.filter((g) => !!g);
|
|
56
|
-
}
|
|
18
|
+
} from "./chunk-M23QBX3V.js";
|
|
57
19
|
|
|
58
20
|
// src/providerCli.ts
|
|
59
21
|
async function addProviderKey(providerId, key, deps) {
|
|
@@ -109,7 +71,6 @@ async function runProviderCommand(args, deps) {
|
|
|
109
71
|
}
|
|
110
72
|
|
|
111
73
|
export {
|
|
112
|
-
listAllModels,
|
|
113
74
|
addProviderKey,
|
|
114
75
|
runProviderCommand
|
|
115
76
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
hasKey,
|
|
4
|
+
providerName
|
|
5
|
+
} from "./chunk-SESBH44L.js";
|
|
6
|
+
import {
|
|
7
|
+
createProvider
|
|
8
|
+
} from "./chunk-GGNXHJL6.js";
|
|
9
|
+
import {
|
|
10
|
+
PRESETS,
|
|
11
|
+
readCredentials
|
|
12
|
+
} from "./chunk-M7LLJQ7M.js";
|
|
13
|
+
import {
|
|
14
|
+
t
|
|
15
|
+
} from "./chunk-M23QBX3V.js";
|
|
16
|
+
|
|
17
|
+
// src/models.ts
|
|
18
|
+
function order(ids) {
|
|
19
|
+
const presetIds = PRESETS.map((p) => p.id);
|
|
20
|
+
const presets = presetIds.filter((id) => ids.includes(id));
|
|
21
|
+
const custom = ids.filter((id) => !presetIds.includes(id)).sort();
|
|
22
|
+
return [...presets, ...custom];
|
|
23
|
+
}
|
|
24
|
+
async function listAllModels(config, env = process.env, opts = {}) {
|
|
25
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
26
|
+
const baseFetch = opts.fetch ?? fetch;
|
|
27
|
+
const untrusted = new Set(config.projectProviders ?? []);
|
|
28
|
+
const ids = order(Object.keys(config.providers)).filter((id) => opts.only ? id === opts.only : !untrusted.has(id) && hasKey(config, id, env));
|
|
29
|
+
const results = await Promise.all(
|
|
30
|
+
ids.map(async (id) => {
|
|
31
|
+
const provider = config.providers[id];
|
|
32
|
+
const apiKey = (opts.keyOverride && id === opts.only ? opts.keyOverride : void 0) || (provider.apiKeyEnv ? env[provider.apiKeyEnv] : void 0) || readCredentials(env)[id] || void 0;
|
|
33
|
+
const local = !provider.apiKeyEnv;
|
|
34
|
+
const timedFetch = ((input, init) => baseFetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }));
|
|
35
|
+
const list = createProvider({ baseURL: provider.baseURL.replace(/\/+$/, ""), apiKey, model: "", fetch: timedFetch }).listModels();
|
|
36
|
+
let timer;
|
|
37
|
+
const timeout = new Promise((_, reject) => {
|
|
38
|
+
timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
const models = await Promise.race([list, timeout]);
|
|
42
|
+
return { providerId: id, providerName: providerName(config, id), models };
|
|
43
|
+
} catch {
|
|
44
|
+
if (local && !opts.only) return void 0;
|
|
45
|
+
return { providerId: id, providerName: providerName(config, id), models: [], error: t("could not be reached") };
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
return results.filter((g) => !!g);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
listAllModels
|
|
56
|
+
};
|