@botconnector/bccli 0.5.0-rc.4 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -203
- package/README.en.md +253 -0
- package/README.md +250 -0
- package/dist/abort.d.ts +5 -0
- package/dist/acp-HWHF25KU.js +221 -0
- package/dist/acp.d.ts +44 -0
- package/dist/agent.d.ts +158 -0
- package/dist/agents.d.ts +4 -0
- package/dist/args-XA77XUMD.js +13 -0
- package/dist/args.d.ts +59 -0
- package/dist/budget.d.ts +26 -0
- package/dist/checkpoints.d.ts +35 -0
- package/dist/chunk-2IP5F3EK.js +56 -0
- package/dist/chunk-5CVHTYBQ.js +270 -0
- package/dist/chunk-6LO6YRDY.js +216 -0
- package/dist/chunk-6PJRWKVI.js +50 -0
- package/dist/chunk-6YRTPXGY.js +8 -0
- package/dist/chunk-7OTFZK4B.js +159 -0
- package/dist/chunk-7U7CMKKI.js +861 -0
- package/dist/chunk-A3NBWIEQ.js +2706 -0
- package/dist/chunk-FY543EQW.js +50 -0
- package/dist/chunk-IDZLHEVL.js +524 -0
- package/dist/chunk-IF5RNXBI.js +341 -0
- package/dist/chunk-M5T4AM4I.js +256 -0
- package/dist/chunk-NGFWBVHD.js +71 -0
- package/dist/chunk-OCEQ2TRJ.js +1075 -0
- package/dist/chunk-OK2USAXX.js +88 -0
- package/dist/chunk-PSO7R5AD.js +10 -0
- package/dist/chunk-RLSQAZBT.js +78 -0
- package/dist/chunk-VPFCIUC6.js +91 -0
- package/dist/chunk-YGBTNDPK.js +202 -0
- package/dist/chunk-YYTFJUO2.js +148 -0
- package/dist/chunk-ZXWRLN7E.js +378 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +251 -0
- package/dist/clipboardImage.d.ts +17 -0
- package/dist/commands.d.ts +16 -0
- package/dist/completion-IRVEVW6H.js +125 -0
- package/dist/completion.d.ts +14 -0
- package/dist/config-OVWCUVY7.js +25 -0
- package/dist/config-S3CZUIBB.js +34 -0
- package/dist/config.d.ts +99 -0
- package/dist/context.d.ts +13 -0
- package/dist/contextWindow.d.ts +3 -0
- package/dist/diff.d.ts +7 -0
- package/dist/exit.d.ts +9 -0
- package/dist/extensions.d.ts +34 -0
- package/dist/ghBotCli-FYKJDJOD.js +680 -0
- package/dist/ghBotCli.d.ts +9 -0
- package/dist/ghBotDispatch.d.ts +68 -0
- package/dist/ghBotRunner.d.ts +82 -0
- package/dist/ghBotServe.d.ts +21 -0
- package/dist/ghBotWebhook.d.ts +45 -0
- package/dist/hooks.d.ts +27 -0
- package/dist/i18n/id.d.ts +1 -0
- package/dist/i18n.d.ts +16 -0
- package/dist/integrationCli-GY73S2K7.js +876 -0
- package/dist/integrationCli.d.ts +9 -0
- package/dist/keybinds.d.ts +22 -0
- package/dist/login-KSN63EM4.js +18 -0
- package/dist/login.d.ts +21 -0
- package/dist/lsp.d.ts +73 -0
- package/dist/mcp/catalog.d.ts +14 -0
- package/dist/mcp/config.d.ts +36 -0
- package/dist/mcp/manager.d.ts +52 -0
- package/dist/mcp/oauth.d.ts +82 -0
- package/dist/mcpCli-VWSCCDWT.js +113 -0
- package/dist/mcpCli.d.ts +3 -0
- package/dist/mcpServer/serve.d.ts +8 -0
- package/dist/mcpServer/server.d.ts +19 -0
- package/dist/models.d.ts +13 -0
- package/dist/modelsCli-4Q6KFWV2.js +36 -0
- package/dist/modelsCli.d.ts +15 -0
- package/dist/npmRunner-LU43GM3W.js +46 -0
- package/dist/npmRunner.d.ts +21 -0
- package/dist/oauthLogin-JWXQZTMZ.js +210 -0
- package/dist/oauthLogin.d.ts +47 -0
- package/dist/onboarding.d.ts +6 -0
- package/dist/permissions.d.ts +30 -0
- package/dist/pluginSpec.d.ts +25 -0
- package/dist/plugins.d.ts +45 -0
- package/dist/pluginsCli-7XWDLJEL.js +288 -0
- package/dist/pluginsCli.d.ts +16 -0
- package/dist/prCli-FH7N26D4.js +94 -0
- package/dist/prCli.d.ts +46 -0
- package/dist/presets.d.ts +8 -0
- package/dist/print.d.ts +17 -0
- package/dist/provider.d.ts +145 -0
- package/dist/providerCli-YMG4G56R.js +15 -0
- package/dist/providerCli.d.ts +17 -0
- package/dist/providers.d.ts +6 -0
- package/dist/reasoning.d.ts +12 -0
- package/dist/repomap.d.ts +10 -0
- package/dist/sdk.d.ts +61 -0
- package/dist/sdk.js +6532 -0
- package/dist/serve-HSFF6MGB.js +178 -0
- package/dist/serve-R2IHP26A.js +532 -0
- package/dist/serve.d.ts +37 -0
- package/dist/serveHeartbeat.d.ts +14 -0
- package/dist/serveProtocol.d.ts +47 -0
- package/dist/session-YM36TGU7.js +12 -0
- package/dist/session.d.ts +33 -0
- package/dist/setup-3YB6EKYX.js +24 -0
- package/dist/setup.d.ts +79 -0
- package/dist/slash/copy.d.ts +1 -0
- package/dist/slash/diff.d.ts +1 -0
- package/dist/slash/doctor.d.ts +18 -0
- package/dist/slash/export.d.ts +2 -0
- package/dist/slash/info.d.ts +46 -0
- package/dist/slash/memory.d.ts +10 -0
- package/dist/statsCli-I4TSDU3X.js +353 -0
- package/dist/statsCli.d.ts +62 -0
- package/dist/textToolCalls.d.ts +6 -0
- package/dist/thinking.d.ts +17 -0
- package/dist/tokenBudget.d.ts +22 -0
- package/dist/tools/bash.d.ts +54 -0
- package/dist/tools/diagnostics.d.ts +13 -0
- package/dist/tools/edit.d.ts +12 -0
- package/dist/tools/fetch.d.ts +41 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/globSafety.d.ts +7 -0
- package/dist/tools/grep.d.ts +25 -0
- package/dist/tools/ignore.d.ts +2 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/paths.d.ts +4 -0
- package/dist/tools/plan.d.ts +10 -0
- package/dist/tools/read.d.ts +16 -0
- package/dist/tools/repomap.d.ts +2 -0
- package/dist/tools/sandbox.d.ts +15 -0
- package/dist/tools/shell.d.ts +18 -0
- package/dist/tools/skill.d.ts +3 -0
- package/dist/tools/task.d.ts +28 -0
- package/dist/tools/todo.d.ts +13 -0
- package/dist/tools/types.d.ts +52 -0
- package/dist/tools/websearch.d.ts +9 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/transferCli-3B4LMOYI.js +73 -0
- package/dist/transferCli.d.ts +23 -0
- package/dist/ui/App.d.ts +7 -0
- package/dist/ui/DiffView.d.ts +3 -0
- package/dist/ui/EditableText.d.ts +7 -0
- package/dist/ui/LinePrompt.d.ts +6 -0
- package/dist/ui/ListPicker.d.ts +11 -0
- package/dist/ui/Markdown.d.ts +8 -0
- package/dist/ui/McpMenu.d.ts +11 -0
- package/dist/ui/ModelPicker.d.ts +6 -0
- package/dist/ui/PermissionPrompt.d.ts +5 -0
- package/dist/ui/PlanApproval.d.ts +5 -0
- package/dist/ui/PromptInput.d.ts +20 -0
- package/dist/ui/ProviderMenu.d.ts +10 -0
- package/dist/ui/Spinner.d.ts +4 -0
- package/dist/ui/StatusBar.d.ts +14 -0
- package/dist/ui/TodoList.d.ts +4 -0
- package/dist/ui/ToolBlock.d.ts +14 -0
- package/dist/ui/complete.d.ts +8 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/lineEdit.d.ts +42 -0
- package/dist/ui/liveCap.d.ts +29 -0
- package/dist/ui/markdownLayout.d.ts +23 -0
- package/dist/ui/replay.d.ts +4 -0
- package/dist/ui/stdinFilter.d.ts +18 -0
- package/dist/ui/table.d.ts +20 -0
- package/dist/ui/theme.d.ts +5 -0
- package/dist/ui/thinkingMouse.d.ts +38 -0
- package/dist/ui/transcript.d.ts +55 -0
- package/dist/ui/tty.d.ts +11 -0
- package/dist/ui/vim.d.ts +26 -0
- package/dist/ui-TAZMRXRI.js +3044 -0
- package/dist/update-XC3B6SJC.js +145 -0
- package/dist/update.d.ts +30 -0
- package/dist/verify.d.ts +16 -0
- package/dist/version.d.ts +1 -0
- package/dist/webBudget.d.ts +25 -0
- package/dist/worktree.d.ts +12 -0
- package/package.json +59 -35
- package/THIRD_PARTY_NOTICES.md +0 -9
- package/branding.mjs +0 -347
- package/cli.mjs +0 -120
- package/core.mjs +0 -260
- package/credentials.mjs +0 -52
- package/gateway.mjs +0 -2
- package/model-command.mjs +0 -61
- package/provider-cli.mjs +0 -77
- package/providers.mjs +0 -166
- package/proxy.mjs +0 -242
- package/startup-menu.mjs +0 -120
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createRuntime
|
|
4
|
+
} from "./chunk-A3NBWIEQ.js";
|
|
5
|
+
import "./chunk-OK2USAXX.js";
|
|
6
|
+
import "./chunk-7U7CMKKI.js";
|
|
7
|
+
import "./chunk-IF5RNXBI.js";
|
|
8
|
+
import "./chunk-PSO7R5AD.js";
|
|
9
|
+
import {
|
|
10
|
+
VERSION
|
|
11
|
+
} from "./chunk-6YRTPXGY.js";
|
|
12
|
+
import {
|
|
13
|
+
LOOPBACK_BIND,
|
|
14
|
+
MAX_FRAME_BYTES,
|
|
15
|
+
bearerOk,
|
|
16
|
+
parseCliArgs,
|
|
17
|
+
parseClientFrame,
|
|
18
|
+
randomToken,
|
|
19
|
+
trustFence
|
|
20
|
+
} from "./chunk-M5T4AM4I.js";
|
|
21
|
+
import "./chunk-YYTFJUO2.js";
|
|
22
|
+
import "./chunk-RLSQAZBT.js";
|
|
23
|
+
import {
|
|
24
|
+
listAllModels
|
|
25
|
+
} from "./chunk-2IP5F3EK.js";
|
|
26
|
+
import "./chunk-FY543EQW.js";
|
|
27
|
+
import "./chunk-YGBTNDPK.js";
|
|
28
|
+
import {
|
|
29
|
+
Session
|
|
30
|
+
} from "./chunk-6LO6YRDY.js";
|
|
31
|
+
import "./chunk-OCEQ2TRJ.js";
|
|
32
|
+
import "./chunk-NGFWBVHD.js";
|
|
33
|
+
import "./chunk-VPFCIUC6.js";
|
|
34
|
+
import {
|
|
35
|
+
bccliHome,
|
|
36
|
+
loadConfig
|
|
37
|
+
} from "./chunk-ZXWRLN7E.js";
|
|
38
|
+
import "./chunk-IDZLHEVL.js";
|
|
39
|
+
|
|
40
|
+
// src/serve.ts
|
|
41
|
+
import { randomBytes } from "crypto";
|
|
42
|
+
import { createServer } from "http";
|
|
43
|
+
import { basename } from "path";
|
|
44
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
45
|
+
|
|
46
|
+
// src/serveHeartbeat.ts
|
|
47
|
+
function createHeartbeat(intervalMs, ping, terminate) {
|
|
48
|
+
let awaitingPong = false;
|
|
49
|
+
let stopped = false;
|
|
50
|
+
let timer = null;
|
|
51
|
+
const tick = () => {
|
|
52
|
+
if (stopped) return;
|
|
53
|
+
if (awaitingPong) {
|
|
54
|
+
terminate();
|
|
55
|
+
timer = null;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
awaitingPong = true;
|
|
59
|
+
ping();
|
|
60
|
+
timer = setTimeout(tick, intervalMs);
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
start() {
|
|
64
|
+
if (stopped || timer) return;
|
|
65
|
+
awaitingPong = true;
|
|
66
|
+
ping();
|
|
67
|
+
timer = setTimeout(tick, intervalMs);
|
|
68
|
+
},
|
|
69
|
+
pong() {
|
|
70
|
+
awaitingPong = false;
|
|
71
|
+
},
|
|
72
|
+
stop() {
|
|
73
|
+
stopped = true;
|
|
74
|
+
if (timer) clearTimeout(timer);
|
|
75
|
+
timer = null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/serve.ts
|
|
81
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
82
|
+
var DISPLAY_MAX = 1e5;
|
|
83
|
+
function json(res, status, value) {
|
|
84
|
+
const body = JSON.stringify(value);
|
|
85
|
+
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
|
|
86
|
+
res.end(body);
|
|
87
|
+
}
|
|
88
|
+
function readBody(req) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
let body = "";
|
|
91
|
+
let size = 0;
|
|
92
|
+
let tooLarge = false;
|
|
93
|
+
req.on("data", (c) => {
|
|
94
|
+
size += c.length;
|
|
95
|
+
if (size > MAX_BODY_BYTES) tooLarge = true;
|
|
96
|
+
else body += c;
|
|
97
|
+
});
|
|
98
|
+
req.on("end", () => resolve(tooLarge ? null : body));
|
|
99
|
+
req.on("error", reject);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function sendFrame(ws, frame) {
|
|
103
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(frame));
|
|
104
|
+
}
|
|
105
|
+
function serveBanner(handle, opts = {}) {
|
|
106
|
+
const supplied = opts.token !== void 0 || (opts.env ?? process.env).BCCLI_SERVE_TOKEN !== void 0;
|
|
107
|
+
return `bccli serve ${handle.url}${supplied ? "" : ` \u2014 Authorization: Bearer ${handle.token}`}
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
async function startServe(opts = {}) {
|
|
111
|
+
const env = opts.env ?? process.env;
|
|
112
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
113
|
+
const host = opts.host ?? "127.0.0.1";
|
|
114
|
+
if (!LOOPBACK_BIND.has(host)) throw new Error(`serve refuses non-loopback --host ${host} (loopback only)`);
|
|
115
|
+
const token = opts.token ?? env.BCCLI_SERVE_TOKEN ?? randomToken();
|
|
116
|
+
const home = bccliHome(env);
|
|
117
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
118
|
+
const allSockets = /* @__PURE__ */ new Set();
|
|
119
|
+
const server = createServer((req, res) => {
|
|
120
|
+
void handleHttp(req, res);
|
|
121
|
+
});
|
|
122
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
123
|
+
server.on("upgrade", (req, socket, head) => {
|
|
124
|
+
const address2 = server.address();
|
|
125
|
+
const port2 = typeof address2 === "object" && address2 ? address2.port : 0;
|
|
126
|
+
const reject = (status, reason) => {
|
|
127
|
+
socket.write(`HTTP/1.1 ${status} ${reason}\r
|
|
128
|
+
Connection: close\r
|
|
129
|
+
\r
|
|
130
|
+
`);
|
|
131
|
+
socket.destroy();
|
|
132
|
+
};
|
|
133
|
+
const fence = trustFence(req.headers, port2);
|
|
134
|
+
if (fence) return reject(403, "Forbidden");
|
|
135
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
136
|
+
if (url.pathname !== "/v1/mux") return reject(404, "Not Found");
|
|
137
|
+
if (!bearerOk(req.headers.authorization, token)) return reject(401, "Unauthorized");
|
|
138
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
139
|
+
});
|
|
140
|
+
wss.on("connection", (ws) => {
|
|
141
|
+
allSockets.add(ws);
|
|
142
|
+
const heartbeat = createHeartbeat(opts.heartbeatMs ?? 2e3, () => ws.ping(), () => ws.terminate());
|
|
143
|
+
ws.on("pong", () => heartbeat.pong());
|
|
144
|
+
ws.on("close", () => heartbeat.stop());
|
|
145
|
+
heartbeat.start();
|
|
146
|
+
const connStreams = /* @__PURE__ */ new Map();
|
|
147
|
+
ws.on("close", () => {
|
|
148
|
+
allSockets.delete(ws);
|
|
149
|
+
for (const stream of connStreams.values()) stream.session.streams.delete(stream);
|
|
150
|
+
connStreams.clear();
|
|
151
|
+
});
|
|
152
|
+
ws.on("message", (data) => {
|
|
153
|
+
const rawFrame = String(data);
|
|
154
|
+
if (rawFrame.length > MAX_FRAME_BYTES) {
|
|
155
|
+
sendFrame(ws, { type: "error", streamId: "", message: "frame exceeds 256 KB" });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const frame = parseClientFrame(rawFrame);
|
|
159
|
+
if ("error" in frame) {
|
|
160
|
+
sendFrame(ws, { type: "error", streamId: "", message: frame.error });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (frame.type === "cancel") {
|
|
164
|
+
const stream2 = connStreams.get(frame.streamId);
|
|
165
|
+
if (!stream2) return;
|
|
166
|
+
connStreams.delete(frame.streamId);
|
|
167
|
+
stream2.session.streams.delete(stream2);
|
|
168
|
+
sendFrame(ws, { type: "end", streamId: frame.streamId });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (connStreams.has(frame.streamId)) {
|
|
172
|
+
sendFrame(ws, { type: "error", streamId: frame.streamId, message: "duplicate open" });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const match = frame.target.match(/^session:(.+)$/);
|
|
176
|
+
const sessionId = match ? decodeURIComponent(match[1]) : "";
|
|
177
|
+
const session = sessionId ? sessions.get(sessionId) ?? attachSession(sessionId, cwd) : void 0;
|
|
178
|
+
if (!session) {
|
|
179
|
+
sendFrame(ws, { type: "error", streamId: frame.streamId, message: `unknown target ${frame.target}` });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const stream = { id: frame.streamId, ws, session };
|
|
183
|
+
connStreams.set(frame.streamId, stream);
|
|
184
|
+
session.streams.add(stream);
|
|
185
|
+
sendFrame(ws, { type: "item", streamId: frame.streamId, value: { type: "ready", sessionId: session.id, host: { home, version: VERSION } } });
|
|
186
|
+
for (const [requestId, p] of session.pending) {
|
|
187
|
+
sendFrame(ws, {
|
|
188
|
+
type: "item",
|
|
189
|
+
streamId: frame.streamId,
|
|
190
|
+
value: { type: "approval_request", requestId, tool: p.req.tool, kind: p.req.kind, target: p.req.target, preview: p.req.preview }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
function broadcast(s, value) {
|
|
196
|
+
for (const stream of s.streams) sendFrame(stream.ws, { type: "item", streamId: stream.id, value });
|
|
197
|
+
}
|
|
198
|
+
async function handleHttp(req, res) {
|
|
199
|
+
const address2 = server.address();
|
|
200
|
+
const port2 = typeof address2 === "object" && address2 ? address2.port : 0;
|
|
201
|
+
const fence = trustFence(req.headers, port2);
|
|
202
|
+
if (fence) {
|
|
203
|
+
jsonError(res, 403, fence);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
207
|
+
if (url.pathname !== "/health" && !bearerOk(req.headers.authorization, token)) {
|
|
208
|
+
jsonError(res, 401, "missing or invalid bearer token");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
await route(req, res, url);
|
|
212
|
+
}
|
|
213
|
+
function jsonError(res, status, message) {
|
|
214
|
+
json(res, status, { error: { message } });
|
|
215
|
+
}
|
|
216
|
+
async function route(req, res, url) {
|
|
217
|
+
const method = req.method ?? "GET";
|
|
218
|
+
if (method === "GET" && url.pathname === "/health") {
|
|
219
|
+
json(res, 200, { ok: true, version: VERSION });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (method === "GET" && url.pathname === "/v1/models") {
|
|
223
|
+
try {
|
|
224
|
+
const groups = opts.provider ? [{ providerId: "injected", providerName: "injected", models: await opts.provider.listModels() }] : await listAllModels(loadConfig(cwd, env), env, { fetch: opts.fetch });
|
|
225
|
+
json(res, 200, groups);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
jsonError(res, 500, error.message);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (url.pathname === "/v1/sessions" && method === "POST") {
|
|
232
|
+
const raw = await readBody(req);
|
|
233
|
+
if (raw === null) {
|
|
234
|
+
jsonError(res, 413, "request body exceeds 1 MB");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
let body = {};
|
|
238
|
+
if (raw) {
|
|
239
|
+
try {
|
|
240
|
+
body = JSON.parse(raw);
|
|
241
|
+
} catch {
|
|
242
|
+
jsonError(res, 400, "invalid JSON body");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (body.permissionMode !== void 0 && body.permissionMode !== "ask" && body.permissionMode !== "bypassPermissions") {
|
|
247
|
+
jsonError(res, 400, "permissionMode must be 'ask' or 'bypassPermissions'");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const s = openSession(body.cwd ?? cwd, body.permissionMode ?? "ask", body.model);
|
|
252
|
+
json(res, 201, { id: s.id, cwd: s.cwd, permissionMode: s.permissionMode });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
jsonError(res, 500, error.message);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (url.pathname === "/v1/sessions" && method === "GET") {
|
|
259
|
+
const listCwd = url.searchParams.get("cwd") ?? cwd;
|
|
260
|
+
const byId = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const { session, mtime, preview } of Session.list(home, listCwd)) {
|
|
262
|
+
const id = basename(session.file, ".jsonl");
|
|
263
|
+
byId.set(id, { id, mtime: mtime.toISOString(), preview });
|
|
264
|
+
}
|
|
265
|
+
for (const s of sessions.values()) {
|
|
266
|
+
if (s.cwd === listCwd && !byId.has(s.id)) {
|
|
267
|
+
byId.set(s.id, { id: s.id, mtime: (/* @__PURE__ */ new Date()).toISOString(), preview: "" });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
json(res, 200, [...byId.values()]);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const one = url.pathname.match(/^\/v1\/sessions\/([^/]+)$/);
|
|
274
|
+
if (one && method === "GET") {
|
|
275
|
+
const id = decodeURIComponent(one[1]);
|
|
276
|
+
const live = sessions.get(id);
|
|
277
|
+
if (live) {
|
|
278
|
+
json(res, 200, { id, cwd: live.cwd, permissionMode: live.permissionMode, messages: live.rt.agent.messages });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const found = findSessionFile(id, url.searchParams.get("cwd") ?? cwd);
|
|
282
|
+
if (!found) {
|
|
283
|
+
jsonError(res, 404, `unknown session ${id}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
json(res, 200, { id, cwd: found.cwd, permissionMode: "ask", messages: found.session.load() });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const messages = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/messages$/);
|
|
290
|
+
if (messages && method === "POST") {
|
|
291
|
+
const raw = await readBody(req);
|
|
292
|
+
if (raw === null) {
|
|
293
|
+
jsonError(res, 413, "request body exceeds 1 MB");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
let body = {};
|
|
297
|
+
if (raw) {
|
|
298
|
+
try {
|
|
299
|
+
body = JSON.parse(raw);
|
|
300
|
+
} catch {
|
|
301
|
+
jsonError(res, 400, "invalid JSON body");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const id = decodeURIComponent(messages[1]);
|
|
306
|
+
let s = sessions.get(id);
|
|
307
|
+
if (!s) s = attachSession(id, url.searchParams.get("cwd") ?? cwd);
|
|
308
|
+
if (!s) {
|
|
309
|
+
jsonError(res, 404, `unknown session ${id}`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (typeof body.prompt !== "string" || body.prompt === "") {
|
|
313
|
+
jsonError(res, 400, "prompt must be a non-empty string");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (s.busy) {
|
|
317
|
+
jsonError(res, 409, `session ${id} is busy with turn ${s.busy.turnId}`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const turnId = randomBytes(6).toString("hex");
|
|
321
|
+
const controller = new AbortController();
|
|
322
|
+
s.busy = { turnId, controller };
|
|
323
|
+
void runTurn(s, body.prompt, controller);
|
|
324
|
+
json(res, 202, { turnId });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const approvals = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/approvals$/);
|
|
328
|
+
if (approvals && method === "POST") {
|
|
329
|
+
const raw = await readBody(req);
|
|
330
|
+
if (raw === null) {
|
|
331
|
+
jsonError(res, 413, "request body exceeds 1 MB");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
let body = {};
|
|
335
|
+
if (raw) {
|
|
336
|
+
try {
|
|
337
|
+
body = JSON.parse(raw);
|
|
338
|
+
} catch {
|
|
339
|
+
jsonError(res, 400, "invalid JSON body");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const answers = ["yes", "no", "session", "all"];
|
|
344
|
+
if (typeof body.requestId !== "string" || !answers.includes(body.answer)) {
|
|
345
|
+
jsonError(res, 400, "requestId must be a string and answer one of 'yes'|'no'|'session'|'all'");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const s = sessions.get(decodeURIComponent(approvals[1]));
|
|
349
|
+
if (!s) {
|
|
350
|
+
jsonError(res, 404, `unknown session ${decodeURIComponent(approvals[1])}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const pending = s.pending.get(body.requestId);
|
|
354
|
+
if (!pending) {
|
|
355
|
+
jsonError(res, 404, `unknown or already answered requestId ${body.requestId}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
s.pending.delete(body.requestId);
|
|
359
|
+
pending.resolve(body.answer);
|
|
360
|
+
json(res, 200, { accepted: true });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const cancel = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/cancel$/);
|
|
364
|
+
if (cancel && method === "POST") {
|
|
365
|
+
const s = sessions.get(decodeURIComponent(cancel[1]));
|
|
366
|
+
if (!s) {
|
|
367
|
+
jsonError(res, 404, `unknown session ${decodeURIComponent(cancel[1])}`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (s.busy) s.busy.controller.abort();
|
|
371
|
+
failPending(s);
|
|
372
|
+
json(res, 200, { cancelled: true });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
jsonError(res, 404, `no route ${method} ${url.pathname}`);
|
|
376
|
+
}
|
|
377
|
+
async function runTurn(s, prompt, controller) {
|
|
378
|
+
let text = "";
|
|
379
|
+
let stopReason = "done";
|
|
380
|
+
const toolCalls = [];
|
|
381
|
+
const byId = /* @__PURE__ */ new Map();
|
|
382
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
383
|
+
const map = (event) => {
|
|
384
|
+
switch (event.type) {
|
|
385
|
+
case "text":
|
|
386
|
+
text += event.delta;
|
|
387
|
+
broadcast(s, { type: "text", delta: event.delta });
|
|
388
|
+
break;
|
|
389
|
+
case "textReplace":
|
|
390
|
+
text = event.text;
|
|
391
|
+
break;
|
|
392
|
+
case "thinking":
|
|
393
|
+
broadcast(s, { type: "thinking", delta: event.delta });
|
|
394
|
+
break;
|
|
395
|
+
case "toolStart": {
|
|
396
|
+
const call = { name: event.tool, target: event.target };
|
|
397
|
+
byId.set(event.id, call);
|
|
398
|
+
toolCalls.push(call);
|
|
399
|
+
broadcast(s, { type: "tool_use", tool: event.tool, target: event.target });
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case "toolEnd": {
|
|
403
|
+
const call = byId.get(event.id);
|
|
404
|
+
if (call) {
|
|
405
|
+
call.output = event.output;
|
|
406
|
+
call.isError = event.isError;
|
|
407
|
+
}
|
|
408
|
+
const item = {
|
|
409
|
+
type: "tool_result",
|
|
410
|
+
tool: event.tool,
|
|
411
|
+
output: event.output,
|
|
412
|
+
isError: event.isError
|
|
413
|
+
};
|
|
414
|
+
if (typeof event.display === "string") {
|
|
415
|
+
item.display = event.display.length > DISPLAY_MAX ? `${event.display.slice(0, DISPLAY_MAX)}
|
|
416
|
+
[truncated]` : event.display;
|
|
417
|
+
}
|
|
418
|
+
broadcast(s, item);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case "usage":
|
|
422
|
+
usage = { inputTokens: event.inputTokens, outputTokens: event.outputTokens };
|
|
423
|
+
broadcast(s, { type: "usage", ...usage });
|
|
424
|
+
break;
|
|
425
|
+
case "stepLimit":
|
|
426
|
+
stopReason = "stepLimit";
|
|
427
|
+
break;
|
|
428
|
+
case "budgetExceeded":
|
|
429
|
+
stopReason = "budgetExceeded";
|
|
430
|
+
break;
|
|
431
|
+
case "aborted":
|
|
432
|
+
stopReason = "aborted";
|
|
433
|
+
break;
|
|
434
|
+
case "error":
|
|
435
|
+
broadcast(s, { type: "error", message: event.message });
|
|
436
|
+
break;
|
|
437
|
+
default:
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
s.rt.agent.onEvent = map;
|
|
442
|
+
try {
|
|
443
|
+
await s.rt.agent.run(prompt, controller.signal);
|
|
444
|
+
} catch {
|
|
445
|
+
stopReason = controller.signal.aborted ? "aborted" : stopReason;
|
|
446
|
+
} finally {
|
|
447
|
+
s.rt.agent.onEvent = () => {
|
|
448
|
+
};
|
|
449
|
+
s.busy = null;
|
|
450
|
+
failPending(s);
|
|
451
|
+
broadcast(s, { type: "result", text, toolCalls, usage, stopReason });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function findSessionFile(id, forCwd) {
|
|
455
|
+
const hit = Session.list(home, forCwd).find(({ session }) => basename(session.file, ".jsonl") === id);
|
|
456
|
+
return hit ? { session: hit.session, cwd: forCwd } : void 0;
|
|
457
|
+
}
|
|
458
|
+
function buildRuntime(forCwd, permissionMode, model) {
|
|
459
|
+
return createRuntime({
|
|
460
|
+
cwd: forCwd,
|
|
461
|
+
args: { ...parseCliArgs([]), allowAll: permissionMode === "bypassPermissions", model },
|
|
462
|
+
env,
|
|
463
|
+
provider: opts.provider,
|
|
464
|
+
fetch: opts.fetch
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function openSession(forCwd, permissionMode, model) {
|
|
468
|
+
return registerSession(buildRuntime(forCwd, permissionMode, model), forCwd, permissionMode);
|
|
469
|
+
}
|
|
470
|
+
function attachSession(id, forCwd) {
|
|
471
|
+
const found = findSessionFile(id, forCwd);
|
|
472
|
+
if (!found) return void 0;
|
|
473
|
+
const rt = buildRuntime(found.cwd, "ask", found.session.lastModel());
|
|
474
|
+
rt.resume(found.session);
|
|
475
|
+
return registerSession(rt, found.cwd, "ask", id);
|
|
476
|
+
}
|
|
477
|
+
function registerSession(rt, forCwd, permissionMode, id) {
|
|
478
|
+
const sessionId = id ?? basename(rt.session.file, ".jsonl");
|
|
479
|
+
const s = {
|
|
480
|
+
id: sessionId,
|
|
481
|
+
cwd: forCwd,
|
|
482
|
+
rt,
|
|
483
|
+
permissionMode,
|
|
484
|
+
busy: null,
|
|
485
|
+
streams: /* @__PURE__ */ new Set(),
|
|
486
|
+
pending: /* @__PURE__ */ new Map()
|
|
487
|
+
};
|
|
488
|
+
if (permissionMode === "bypassPermissions") {
|
|
489
|
+
rt.agent.askPermission = async () => "yes";
|
|
490
|
+
} else {
|
|
491
|
+
rt.agent.askPermission = (req) => new Promise((resolve) => {
|
|
492
|
+
const requestId = randomBytes(6).toString("hex");
|
|
493
|
+
s.pending.set(requestId, { resolve, req });
|
|
494
|
+
broadcast(s, {
|
|
495
|
+
type: "approval_request",
|
|
496
|
+
requestId,
|
|
497
|
+
tool: req.tool,
|
|
498
|
+
kind: req.kind,
|
|
499
|
+
target: req.target,
|
|
500
|
+
preview: req.preview
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
sessions.set(sessionId, s);
|
|
505
|
+
return s;
|
|
506
|
+
}
|
|
507
|
+
function failPending(s) {
|
|
508
|
+
for (const [, p] of s.pending) p.resolve("no");
|
|
509
|
+
s.pending.clear();
|
|
510
|
+
}
|
|
511
|
+
await new Promise(
|
|
512
|
+
(resolve) => server.listen(opts.port ?? 8787, host === "localhost" ? "127.0.0.1" : host, resolve)
|
|
513
|
+
);
|
|
514
|
+
const address = server.address();
|
|
515
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
516
|
+
return {
|
|
517
|
+
port,
|
|
518
|
+
token,
|
|
519
|
+
url: `http://127.0.0.1:${port}`,
|
|
520
|
+
close: () => new Promise((resolve) => {
|
|
521
|
+
for (const s of sessions.values()) failPending(s);
|
|
522
|
+
for (const ws of allSockets) ws.terminate();
|
|
523
|
+
allSockets.clear();
|
|
524
|
+
server.closeAllConnections();
|
|
525
|
+
server.close(() => resolve());
|
|
526
|
+
})
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
export {
|
|
530
|
+
serveBanner,
|
|
531
|
+
startServe
|
|
532
|
+
};
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Provider } from './provider';
|
|
2
|
+
export interface ServeOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
host?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
provider?: Provider;
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
heartbeatMs?: number;
|
|
11
|
+
err?: (s: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ServeHandle {
|
|
14
|
+
port: number;
|
|
15
|
+
token: string;
|
|
16
|
+
url: string;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export type PermissionMode = 'ask' | 'bypassPermissions';
|
|
20
|
+
/**
|
|
21
|
+
* One-line terminal banner for `bccli serve`. A token the user supplied (`--token` or
|
|
22
|
+
* `BCCLI_SERVE_TOKEN`) is left out so it cannot leak into logs; a token BCCLI generated
|
|
23
|
+
* itself must be printed or nobody could connect.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serveBanner(handle: {
|
|
26
|
+
url: string;
|
|
27
|
+
token: string;
|
|
28
|
+
}, opts?: {
|
|
29
|
+
token?: string;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
}): string;
|
|
32
|
+
/**
|
|
33
|
+
* Starts the loopback session server (HTTP unary + WS mux at /v1/mux). Refuses
|
|
34
|
+
* non-loopback bind addresses; every request passes the DSH-style trust fence
|
|
35
|
+
* (403) and, except /health, the bearer-token check (401).
|
|
36
|
+
*/
|
|
37
|
+
export declare function startServe(opts?: ServeOptions): Promise<ServeHandle>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Heartbeat {
|
|
2
|
+
/** Sends the first ping and schedules the check loop. Idempotent until stop(). */
|
|
3
|
+
start(): void;
|
|
4
|
+
/** Marks the last ping as answered (ws 'pong' event). */
|
|
5
|
+
pong(): void;
|
|
6
|
+
/** Clears the timer; no further pings or terminations. */
|
|
7
|
+
stop(): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* ws heartbeat with deadline: every interval a ping goes out; a ping left
|
|
11
|
+
* unanswered by the next tick terminates the socket (connection, not stream).
|
|
12
|
+
* Modeled on the DSH gateway's `websocketHeartbeatIntervalMs` behavior.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createHeartbeat(intervalMs: number, ping: () => void, terminate: () => void): Heartbeat;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Wire frames of the /v1/mux WebSocket (subset of the DSH stream protocol). */
|
|
2
|
+
export type ClientFrame = {
|
|
3
|
+
type: 'open';
|
|
4
|
+
streamId: string;
|
|
5
|
+
target: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'cancel';
|
|
8
|
+
streamId: string;
|
|
9
|
+
};
|
|
10
|
+
export type ServerFrame = {
|
|
11
|
+
type: 'item';
|
|
12
|
+
streamId: string;
|
|
13
|
+
value: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'end';
|
|
16
|
+
streamId: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'error';
|
|
19
|
+
streamId: string;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
/** Uplink frame size limit; oversized frames error their stream without closing the socket. */
|
|
23
|
+
export declare const MAX_FRAME_BYTES = 262144;
|
|
24
|
+
/**
|
|
25
|
+
* Parses one uplink mux frame. Malformed JSON, unknown types, an empty streamId,
|
|
26
|
+
* or an `open` without a target produce `{ error }` instead of throwing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseClientFrame(raw: string): ClientFrame | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
/** Addresses `bccli serve` is allowed to bind (loopback only; 0.0.0.0 is refused). */
|
|
32
|
+
export declare const LOOPBACK_BIND: Set<string>;
|
|
33
|
+
/**
|
|
34
|
+
* DSH-style trust fence: loopback Host with a port matching the listener, a
|
|
35
|
+
* same-machine Origin when attached, and no cross-site sec-fetch-site. Returns
|
|
36
|
+
* null when trusted, else the 403 reason. Never establishes identity — the
|
|
37
|
+
* bearer token does.
|
|
38
|
+
*/
|
|
39
|
+
export declare function trustFence(headers: {
|
|
40
|
+
host?: string;
|
|
41
|
+
origin?: string;
|
|
42
|
+
'sec-fetch-site'?: string;
|
|
43
|
+
}, port: number): string | null;
|
|
44
|
+
/** Timing-safe `Authorization: Bearer <token>` check. */
|
|
45
|
+
export declare function bearerOk(header: string | undefined, token: string): boolean;
|
|
46
|
+
/** Fresh 32-byte hex serve token. */
|
|
47
|
+
export declare function randomToken(): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ChatMessage, type Usage } from './provider';
|
|
2
|
+
export declare class Session {
|
|
3
|
+
readonly file: string;
|
|
4
|
+
constructor(file: string);
|
|
5
|
+
get id(): string;
|
|
6
|
+
static create(home: string, cwd: string, now?: Date): Session;
|
|
7
|
+
static list(home: string, cwd: string): {
|
|
8
|
+
session: Session;
|
|
9
|
+
mtime: Date;
|
|
10
|
+
preview: string;
|
|
11
|
+
}[];
|
|
12
|
+
static latest(home: string, cwd: string): Session | undefined;
|
|
13
|
+
private write;
|
|
14
|
+
append(message: ChatMessage): void;
|
|
15
|
+
appendUsage(usage: Usage & {
|
|
16
|
+
model?: string;
|
|
17
|
+
}): void;
|
|
18
|
+
reset(): void;
|
|
19
|
+
private records;
|
|
20
|
+
/** What the model continues from: everything after the last reset (after a compaction that is the summary and the recent turns). */
|
|
21
|
+
load(): ChatMessage[];
|
|
22
|
+
/** Model of the most recent turn that recorded usage — serve attach continues a session with its own model. */
|
|
23
|
+
lastModel(): string | undefined;
|
|
24
|
+
/** Accumulated token usage since the last non-compaction reset. */
|
|
25
|
+
totalUsage(): Usage;
|
|
26
|
+
/**
|
|
27
|
+
* What the user sees on resume: the whole conversation. Compaction only shrinks what the model is sent, so
|
|
28
|
+
* its reset does not hide the older turns here. A real reset (/clear, rewind) still starts over, the
|
|
29
|
+
* synthetic summary pair is never shown, and the recent turns a compaction re-writes are not shown twice.
|
|
30
|
+
*/
|
|
31
|
+
loadHistory(): ChatMessage[];
|
|
32
|
+
}
|
|
33
|
+
export declare function pruneSessions(home: string, now?: number, maxAgeDays?: number): number;
|