@botconnector/bccli 0.4.0-beta.5 → 0.4.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +97 -2
  2. package/dist/acp-NWP4FFTG.js +182 -0
  3. package/dist/args-2R45YZ42.js +13 -0
  4. package/dist/{chunk-U7DLMVPZ.js → chunk-2QYHSESB.js} +2 -1
  5. package/dist/{chunk-UESQNU4A.js → chunk-4WVIPZFV.js} +14 -9
  6. package/dist/chunk-7F3IWBPQ.js +191 -0
  7. package/dist/chunk-DP7EBW53.js +341 -0
  8. package/dist/{chunk-SJH3YLW4.js → chunk-F6ACN47V.js} +60 -6
  9. package/dist/chunk-FTFVW7XD.js +235 -0
  10. package/dist/{chunk-ELML4KVQ.js → chunk-GAFJEQTJ.js} +32 -2
  11. package/dist/{chunk-NGLKANMD.js → chunk-H73FO5YK.js} +24 -2
  12. package/dist/chunk-KD2LMIS4.js +145 -0
  13. package/dist/chunk-KIZC5XV4.js +2141 -0
  14. package/dist/chunk-N7KP6RNP.js +76 -0
  15. package/dist/chunk-NDEJUL44.js +361 -0
  16. package/dist/chunk-PSO7R5AD.js +10 -0
  17. package/dist/chunk-QXOV4DUZ.js +56 -0
  18. package/dist/chunk-QZKLZELZ.js +64 -0
  19. package/dist/chunk-R4BJBTWC.js +940 -0
  20. package/dist/chunk-SNKVS26H.js +63 -0
  21. package/dist/chunk-TQWCM5RO.js +231 -0
  22. package/dist/chunk-VU5HNQHX.js +8 -0
  23. package/dist/chunk-XLC6T4SX.js +500 -0
  24. package/dist/chunk-YIYX3FFS.js +733 -0
  25. package/dist/cli.js +157 -1281
  26. package/dist/completion-OV3MW2N4.js +125 -0
  27. package/dist/{config-NPSYFAFD.js → config-AZZFOXKV.js} +5 -2
  28. package/dist/config-H5A7XHHK.js +30 -0
  29. package/dist/ghBotCli-7WDYF4L7.js +677 -0
  30. package/dist/integrationCli-ZMTSILYF.js +685 -0
  31. package/dist/login-W4Y6WHLV.js +16 -0
  32. package/dist/{mcpCli-Q2H63ISP.js → mcpCli-5QISTOFK.js} +42 -9
  33. package/dist/modelsCli-FZMBD4OE.js +36 -0
  34. package/dist/npmRunner-LU43GM3W.js +46 -0
  35. package/dist/oauthLogin-VESMXG4M.js +210 -0
  36. package/dist/pluginsCli-GEHR7FQR.js +284 -0
  37. package/dist/prCli-JXA7DJW4.js +72 -0
  38. package/dist/providerCli-EUZGKP5F.js +15 -0
  39. package/dist/sdk.js +5486 -0
  40. package/dist/serve-6KX4MXHC.js +178 -0
  41. package/dist/serve-YSUJD57D.js +515 -0
  42. package/dist/session-F6MG3UCU.js +12 -0
  43. package/dist/setup-LLWREJQH.js +24 -0
  44. package/dist/statsCli-H4HEY5ID.js +353 -0
  45. package/dist/transferCli-2XUKBG2L.js +71 -0
  46. package/dist/ui-6HJVYI46.js +2370 -0
  47. package/dist/update-O4WCLKOO.js +109 -0
  48. package/package.json +24 -3
  49. package/dist/args-FCLPQ7TO.js +0 -11
  50. package/dist/chunk-5O4HD7IU.js +0 -253
  51. package/dist/chunk-C5AFWX7H.js +0 -328
  52. package/dist/chunk-GW36UMHZ.js +0 -52
  53. package/dist/chunk-RZUJYAQV.js +0 -112
  54. package/dist/chunk-UN7QXPCB.js +0 -82
  55. package/dist/login-F4G5QXOU.js +0 -9
  56. package/dist/providerCli-MXV6PD6F.js +0 -11
  57. package/dist/session-7UB77CEG.js +0 -9
  58. package/dist/ui-RYTSKEIA.js +0 -1344
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createFetchTool,
4
+ createWebSearchTool,
5
+ isPrivateHost
6
+ } from "./chunk-DP7EBW53.js";
7
+ import "./chunk-PSO7R5AD.js";
8
+ import {
9
+ loadConfig,
10
+ readCredentials
11
+ } from "./chunk-NDEJUL44.js";
12
+ import {
13
+ t
14
+ } from "./chunk-XLC6T4SX.js";
15
+
16
+ // src/mcpServer/serve.ts
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
+
19
+ // src/mcpServer/server.ts
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { z } from "zod";
22
+ var CATALOG_URL = "https://botconnector.id/data/cloud-models.json";
23
+ var PRICING_URL = "https://botconnector.id/data/payg-pricing.json";
24
+ var CACHE_TTL_MS = 60 * 6e4;
25
+ var ctxLabel = (context) => {
26
+ if (!context || !Number.isFinite(context)) return "?";
27
+ return context >= 1e3 ? `${Math.round(context / 1e3)}k` : String(context);
28
+ };
29
+ var usd = (v) => typeof v === "number" ? `$${v}` : "?";
30
+ function createMcpServer(deps) {
31
+ const server = new McpServer({ name: "botconnector", version: "0.1.0" });
32
+ const fetchFn = deps.fetchFn ?? fetch;
33
+ const now = deps.now ?? Date.now;
34
+ let cache;
35
+ const searchTool = createWebSearchTool({
36
+ botconnector: () => deps.apiKey ? { baseURL: deps.baseUrl, apiKey: deps.apiKey } : void 0,
37
+ keenableURL: deps.keenableURL
38
+ });
39
+ const pageTool = createFetchTool(deps.fetchTool);
40
+ const toolCtx = () => ({
41
+ cwd: ".",
42
+ signal: AbortSignal.timeout(3e4),
43
+ readFiles: /* @__PURE__ */ new Set()
44
+ });
45
+ server.registerTool(
46
+ "bc_models",
47
+ {
48
+ description: "List BotConnector cloud models with context window and pay-as-you-go price (USD per 1M tokens).",
49
+ inputSchema: {},
50
+ annotations: { readOnlyHint: true }
51
+ },
52
+ async () => {
53
+ if (cache && now() - cache.at < CACHE_TTL_MS) {
54
+ return { content: [{ type: "text", text: cache.text }] };
55
+ }
56
+ const [catalogRes, pricingRes] = await Promise.all([fetchFn(CATALOG_URL), fetchFn(PRICING_URL)]);
57
+ if (!catalogRes.ok) throw new Error(`catalog HTTP ${catalogRes.status}`);
58
+ const catalog = await catalogRes.json();
59
+ const pricing = pricingRes.ok ? await pricingRes.json() : { tokenRates: [] };
60
+ const rates = new Map((pricing.tokenRates ?? []).map((r) => [r.id, r]));
61
+ const lines = (catalog.models ?? []).map((m) => {
62
+ const tier = rates.get(m.id)?.tiers?.[0];
63
+ const price = tier ? `in ${usd(tier.inputPerMTokensUsd)}/M out ${usd(tier.outputPerMTokensUsd)}/M` : "price n/a";
64
+ const access = m.access ? ` \xB7 ${m.access}` : "";
65
+ return `${m.id} \xB7 ctx ${ctxLabel(m.context)} \xB7 ${price}${access}`;
66
+ });
67
+ const text = lines.length ? lines.join("\n") : "No models in the catalog.";
68
+ cache = { at: now(), text };
69
+ return { content: [{ type: "text", text }] };
70
+ }
71
+ );
72
+ server.registerTool(
73
+ "bc_search",
74
+ {
75
+ description: "Search the web and return ranked results (title, url, snippet). Use it to discover pages, then bc_fetch to read one.",
76
+ inputSchema: {
77
+ query: z.string().min(1).max(500).describe("Search query"),
78
+ max_results: z.number().int().min(1).max(8).optional().describe("Number of results (default 6)")
79
+ },
80
+ annotations: { readOnlyHint: true }
81
+ },
82
+ async ({ query, max_results }) => {
83
+ const r = await searchTool.run({ query, max_results }, toolCtx());
84
+ return { content: [{ type: "text", text: r.output }], ...r.isError ? { isError: true } : {} };
85
+ }
86
+ );
87
+ server.registerTool(
88
+ "bc_fetch",
89
+ {
90
+ description: "Fetch a URL and return its text content (HTML tags stripped).",
91
+ inputSchema: { url: z.string().min(1).describe("Absolute http(s) URL") },
92
+ annotations: { readOnlyHint: true }
93
+ },
94
+ async ({ url }) => {
95
+ if (!/^https?:\/\//i.test(url)) {
96
+ return { content: [{ type: "text", text: "URL must start with http:// or https://" }], isError: true };
97
+ }
98
+ let host;
99
+ try {
100
+ host = new URL(url).hostname;
101
+ } catch {
102
+ return { content: [{ type: "text", text: "Invalid URL." }], isError: true };
103
+ }
104
+ if (isPrivateHost(host)) {
105
+ return {
106
+ content: [{ type: "text", text: `Blocked: ${host} is a private host. bc_fetch only fetches public URLs.` }],
107
+ isError: true
108
+ };
109
+ }
110
+ const r = await pageTool.run({ url }, toolCtx());
111
+ return { content: [{ type: "text", text: r.output }], ...r.isError ? { isError: true } : {} };
112
+ }
113
+ );
114
+ server.registerTool(
115
+ "bc_chat",
116
+ {
117
+ description: "Send a prompt to a BotConnector model and get the reply. Returns the answer plus token usage. The model pays from the same API key.",
118
+ inputSchema: {
119
+ prompt: z.string().min(1).max(1e5).describe("The user message"),
120
+ model: z.string().optional().describe("Model id (default: the configured model)"),
121
+ max_tokens: z.number().int().min(1).max(4096).optional().describe("Reply budget (default 1024, max 4096)")
122
+ },
123
+ annotations: { readOnlyHint: false, destructiveHint: false }
124
+ },
125
+ async ({ prompt, model, max_tokens }) => {
126
+ const res = await fetchFn(`${deps.baseUrl}/chat/completions`, {
127
+ method: "POST",
128
+ headers: { "content-type": "application/json", authorization: `Bearer ${deps.apiKey}` },
129
+ body: JSON.stringify({
130
+ model: model ?? deps.defaultModel,
131
+ messages: [{ role: "user", content: prompt }],
132
+ max_tokens: max_tokens ?? 1024,
133
+ stream: false
134
+ }),
135
+ signal: AbortSignal.timeout(3e4)
136
+ });
137
+ const body = await res.json().catch(() => ({}));
138
+ if (!res.ok) {
139
+ const message = body.error?.message ?? `HTTP ${res.status}`;
140
+ return { content: [{ type: "text", text: `bc_chat failed: ${message}` }], isError: true };
141
+ }
142
+ const reply = body.choices?.[0]?.message?.content ?? "";
143
+ const usage = body.usage ? `
144
+
145
+ [usage: ${body.usage.prompt_tokens ?? 0} in / ${body.usage.completion_tokens ?? 0} out]` : "";
146
+ return { content: [{ type: "text", text: reply + usage }] };
147
+ }
148
+ );
149
+ return server;
150
+ }
151
+
152
+ // src/mcpServer/serve.ts
153
+ var DEFAULT_BASE = "https://api.botconnector.id/v1";
154
+ var FALLBACK_MODEL = "glm-5.3-flash";
155
+ async function runMcpServe(args, deps) {
156
+ const apiKey = readCredentials(deps.env)["bc-cloud"] || deps.env.BOTCONNECTOR_API_KEY;
157
+ if (!apiKey) {
158
+ deps.err(t("No BotConnector API key yet. Run bccli login bc-cloud first."));
159
+ return 1;
160
+ }
161
+ const config = loadConfig(deps.cwd, deps.env);
162
+ const providerBase = config.providers["bc-cloud"]?.baseURL ?? DEFAULT_BASE;
163
+ const baseUrl = (args.baseUrl ?? providerBase).replace(/\/+$/, "");
164
+ const defaultModel = config.model.startsWith("bc-cloud/") ? config.model.slice("bc-cloud/".length) : FALLBACK_MODEL;
165
+ const server = createMcpServer({ baseUrl, apiKey, defaultModel, fetchFn: deps.fetch });
166
+ await server.connect(deps.transport ?? new StdioServerTransport());
167
+ deps.err(`mcp serve: 4 tools, base=${baseUrl}`);
168
+ if (deps.transport) return 0;
169
+ await new Promise((resolve) => {
170
+ const stdin = process.stdin;
171
+ stdin.on("end", () => resolve());
172
+ stdin.on("close", () => resolve());
173
+ });
174
+ return 0;
175
+ }
176
+ export {
177
+ runMcpServe
178
+ };
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createRuntime
4
+ } from "./chunk-KIZC5XV4.js";
5
+ import "./chunk-QZKLZELZ.js";
6
+ import "./chunk-YIYX3FFS.js";
7
+ import "./chunk-DP7EBW53.js";
8
+ import "./chunk-PSO7R5AD.js";
9
+ import {
10
+ VERSION
11
+ } from "./chunk-VU5HNQHX.js";
12
+ import {
13
+ LOOPBACK_BIND,
14
+ MAX_FRAME_BYTES,
15
+ bearerOk,
16
+ parseCliArgs,
17
+ parseClientFrame,
18
+ randomToken,
19
+ trustFence
20
+ } from "./chunk-FTFVW7XD.js";
21
+ import "./chunk-GAFJEQTJ.js";
22
+ import "./chunk-N7KP6RNP.js";
23
+ import {
24
+ listAllModels
25
+ } from "./chunk-QXOV4DUZ.js";
26
+ import "./chunk-2QYHSESB.js";
27
+ import "./chunk-7F3IWBPQ.js";
28
+ import {
29
+ Session
30
+ } from "./chunk-F6ACN47V.js";
31
+ import "./chunk-R4BJBTWC.js";
32
+ import "./chunk-SNKVS26H.js";
33
+ import "./chunk-H73FO5YK.js";
34
+ import {
35
+ bccliHome,
36
+ loadConfig
37
+ } from "./chunk-NDEJUL44.js";
38
+ import "./chunk-XLC6T4SX.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
+ function json(res, status, value) {
83
+ const body = JSON.stringify(value);
84
+ res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
85
+ res.end(body);
86
+ }
87
+ function readBody(req) {
88
+ return new Promise((resolve, reject) => {
89
+ let body = "";
90
+ let size = 0;
91
+ let tooLarge = false;
92
+ req.on("data", (c) => {
93
+ size += c.length;
94
+ if (size > MAX_BODY_BYTES) tooLarge = true;
95
+ else body += c;
96
+ });
97
+ req.on("end", () => resolve(tooLarge ? null : body));
98
+ req.on("error", reject);
99
+ });
100
+ }
101
+ function sendFrame(ws, frame) {
102
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(frame));
103
+ }
104
+ async function startServe(opts = {}) {
105
+ const env = opts.env ?? process.env;
106
+ const cwd = opts.cwd ?? process.cwd();
107
+ const host = opts.host ?? "127.0.0.1";
108
+ if (!LOOPBACK_BIND.has(host)) throw new Error(`serve refuses non-loopback --host ${host} (loopback only)`);
109
+ const token = opts.token ?? env.BCCLI_SERVE_TOKEN ?? randomToken();
110
+ const home = bccliHome(env);
111
+ const sessions = /* @__PURE__ */ new Map();
112
+ const allSockets = /* @__PURE__ */ new Set();
113
+ const server = createServer((req, res) => {
114
+ void handleHttp(req, res);
115
+ });
116
+ const wss = new WebSocketServer({ noServer: true });
117
+ server.on("upgrade", (req, socket, head) => {
118
+ const address2 = server.address();
119
+ const port2 = typeof address2 === "object" && address2 ? address2.port : 0;
120
+ const reject = (status, reason) => {
121
+ socket.write(`HTTP/1.1 ${status} ${reason}\r
122
+ Connection: close\r
123
+ \r
124
+ `);
125
+ socket.destroy();
126
+ };
127
+ const fence = trustFence(req.headers, port2);
128
+ if (fence) return reject(403, "Forbidden");
129
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
130
+ if (url.pathname !== "/v1/mux") return reject(404, "Not Found");
131
+ if (!bearerOk(req.headers.authorization, token)) return reject(401, "Unauthorized");
132
+ wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
133
+ });
134
+ wss.on("connection", (ws) => {
135
+ allSockets.add(ws);
136
+ const heartbeat = createHeartbeat(opts.heartbeatMs ?? 2e3, () => ws.ping(), () => ws.terminate());
137
+ ws.on("pong", () => heartbeat.pong());
138
+ ws.on("close", () => heartbeat.stop());
139
+ heartbeat.start();
140
+ const connStreams = /* @__PURE__ */ new Map();
141
+ ws.on("close", () => {
142
+ allSockets.delete(ws);
143
+ for (const stream of connStreams.values()) stream.session.streams.delete(stream);
144
+ connStreams.clear();
145
+ });
146
+ ws.on("message", (data) => {
147
+ const rawFrame = String(data);
148
+ if (rawFrame.length > MAX_FRAME_BYTES) {
149
+ sendFrame(ws, { type: "error", streamId: "", message: "frame exceeds 256 KB" });
150
+ return;
151
+ }
152
+ const frame = parseClientFrame(rawFrame);
153
+ if ("error" in frame) {
154
+ sendFrame(ws, { type: "error", streamId: "", message: frame.error });
155
+ return;
156
+ }
157
+ if (frame.type === "cancel") {
158
+ const stream2 = connStreams.get(frame.streamId);
159
+ if (!stream2) return;
160
+ connStreams.delete(frame.streamId);
161
+ stream2.session.streams.delete(stream2);
162
+ sendFrame(ws, { type: "end", streamId: frame.streamId });
163
+ return;
164
+ }
165
+ if (connStreams.has(frame.streamId)) {
166
+ sendFrame(ws, { type: "error", streamId: frame.streamId, message: "duplicate open" });
167
+ return;
168
+ }
169
+ const match = frame.target.match(/^session:(.+)$/);
170
+ const sessionId = match ? decodeURIComponent(match[1]) : "";
171
+ const session = sessionId ? sessions.get(sessionId) ?? attachSession(sessionId, cwd) : void 0;
172
+ if (!session) {
173
+ sendFrame(ws, { type: "error", streamId: frame.streamId, message: `unknown target ${frame.target}` });
174
+ return;
175
+ }
176
+ const stream = { id: frame.streamId, ws, session };
177
+ connStreams.set(frame.streamId, stream);
178
+ session.streams.add(stream);
179
+ sendFrame(ws, { type: "item", streamId: frame.streamId, value: { type: "ready", sessionId: session.id, host: { home, version: VERSION } } });
180
+ for (const [requestId, p] of session.pending) {
181
+ sendFrame(ws, {
182
+ type: "item",
183
+ streamId: frame.streamId,
184
+ value: { type: "approval_request", requestId, tool: p.req.tool, kind: p.req.kind, target: p.req.target, preview: p.req.preview }
185
+ });
186
+ }
187
+ });
188
+ });
189
+ function broadcast(s, value) {
190
+ for (const stream of s.streams) sendFrame(stream.ws, { type: "item", streamId: stream.id, value });
191
+ }
192
+ async function handleHttp(req, res) {
193
+ const address2 = server.address();
194
+ const port2 = typeof address2 === "object" && address2 ? address2.port : 0;
195
+ const fence = trustFence(req.headers, port2);
196
+ if (fence) {
197
+ jsonError(res, 403, fence);
198
+ return;
199
+ }
200
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
201
+ if (url.pathname !== "/health" && !bearerOk(req.headers.authorization, token)) {
202
+ jsonError(res, 401, "missing or invalid bearer token");
203
+ return;
204
+ }
205
+ await route(req, res, url);
206
+ }
207
+ function jsonError(res, status, message) {
208
+ json(res, status, { error: { message } });
209
+ }
210
+ async function route(req, res, url) {
211
+ const method = req.method ?? "GET";
212
+ if (method === "GET" && url.pathname === "/health") {
213
+ json(res, 200, { ok: true, version: VERSION });
214
+ return;
215
+ }
216
+ if (method === "GET" && url.pathname === "/v1/models") {
217
+ try {
218
+ const groups = opts.provider ? [{ providerId: "injected", providerName: "injected", models: await opts.provider.listModels() }] : await listAllModels(loadConfig(cwd, env), env, { fetch: opts.fetch });
219
+ json(res, 200, groups);
220
+ } catch (error) {
221
+ jsonError(res, 500, error.message);
222
+ }
223
+ return;
224
+ }
225
+ if (url.pathname === "/v1/sessions" && method === "POST") {
226
+ const raw = await readBody(req);
227
+ if (raw === null) {
228
+ jsonError(res, 413, "request body exceeds 1 MB");
229
+ return;
230
+ }
231
+ let body = {};
232
+ if (raw) {
233
+ try {
234
+ body = JSON.parse(raw);
235
+ } catch {
236
+ jsonError(res, 400, "invalid JSON body");
237
+ return;
238
+ }
239
+ }
240
+ if (body.permissionMode !== void 0 && body.permissionMode !== "ask" && body.permissionMode !== "bypassPermissions") {
241
+ jsonError(res, 400, "permissionMode must be 'ask' or 'bypassPermissions'");
242
+ return;
243
+ }
244
+ try {
245
+ const s = openSession(body.cwd ?? cwd, body.permissionMode ?? "ask", body.model);
246
+ json(res, 201, { id: s.id, cwd: s.cwd, permissionMode: s.permissionMode });
247
+ } catch (error) {
248
+ jsonError(res, 500, error.message);
249
+ }
250
+ return;
251
+ }
252
+ if (url.pathname === "/v1/sessions" && method === "GET") {
253
+ const listCwd = url.searchParams.get("cwd") ?? cwd;
254
+ const byId = /* @__PURE__ */ new Map();
255
+ for (const { session, mtime, preview } of Session.list(home, listCwd)) {
256
+ const id = basename(session.file, ".jsonl");
257
+ byId.set(id, { id, mtime: mtime.toISOString(), preview });
258
+ }
259
+ for (const s of sessions.values()) {
260
+ if (s.cwd === listCwd && !byId.has(s.id)) {
261
+ byId.set(s.id, { id: s.id, mtime: (/* @__PURE__ */ new Date()).toISOString(), preview: "" });
262
+ }
263
+ }
264
+ json(res, 200, [...byId.values()]);
265
+ return;
266
+ }
267
+ const one = url.pathname.match(/^\/v1\/sessions\/([^/]+)$/);
268
+ if (one && method === "GET") {
269
+ const id = decodeURIComponent(one[1]);
270
+ const live = sessions.get(id);
271
+ if (live) {
272
+ json(res, 200, { id, cwd: live.cwd, permissionMode: live.permissionMode, messages: live.rt.agent.messages });
273
+ return;
274
+ }
275
+ const found = findSessionFile(id, url.searchParams.get("cwd") ?? cwd);
276
+ if (!found) {
277
+ jsonError(res, 404, `unknown session ${id}`);
278
+ return;
279
+ }
280
+ json(res, 200, { id, cwd: found.cwd, permissionMode: "ask", messages: found.session.load() });
281
+ return;
282
+ }
283
+ const messages = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/messages$/);
284
+ if (messages && method === "POST") {
285
+ const raw = await readBody(req);
286
+ if (raw === null) {
287
+ jsonError(res, 413, "request body exceeds 1 MB");
288
+ return;
289
+ }
290
+ let body = {};
291
+ if (raw) {
292
+ try {
293
+ body = JSON.parse(raw);
294
+ } catch {
295
+ jsonError(res, 400, "invalid JSON body");
296
+ return;
297
+ }
298
+ }
299
+ const id = decodeURIComponent(messages[1]);
300
+ let s = sessions.get(id);
301
+ if (!s) s = attachSession(id, url.searchParams.get("cwd") ?? cwd);
302
+ if (!s) {
303
+ jsonError(res, 404, `unknown session ${id}`);
304
+ return;
305
+ }
306
+ if (typeof body.prompt !== "string" || body.prompt === "") {
307
+ jsonError(res, 400, "prompt must be a non-empty string");
308
+ return;
309
+ }
310
+ if (s.busy) {
311
+ jsonError(res, 409, `session ${id} is busy with turn ${s.busy.turnId}`);
312
+ return;
313
+ }
314
+ const turnId = randomBytes(6).toString("hex");
315
+ const controller = new AbortController();
316
+ s.busy = { turnId, controller };
317
+ void runTurn(s, body.prompt, controller);
318
+ json(res, 202, { turnId });
319
+ return;
320
+ }
321
+ const approvals = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/approvals$/);
322
+ if (approvals && method === "POST") {
323
+ const raw = await readBody(req);
324
+ if (raw === null) {
325
+ jsonError(res, 413, "request body exceeds 1 MB");
326
+ return;
327
+ }
328
+ let body = {};
329
+ if (raw) {
330
+ try {
331
+ body = JSON.parse(raw);
332
+ } catch {
333
+ jsonError(res, 400, "invalid JSON body");
334
+ return;
335
+ }
336
+ }
337
+ const answers = ["yes", "no", "session", "all"];
338
+ if (typeof body.requestId !== "string" || !answers.includes(body.answer)) {
339
+ jsonError(res, 400, "requestId must be a string and answer one of 'yes'|'no'|'session'|'all'");
340
+ return;
341
+ }
342
+ const s = sessions.get(decodeURIComponent(approvals[1]));
343
+ if (!s) {
344
+ jsonError(res, 404, `unknown session ${decodeURIComponent(approvals[1])}`);
345
+ return;
346
+ }
347
+ const pending = s.pending.get(body.requestId);
348
+ if (!pending) {
349
+ jsonError(res, 404, `unknown or already answered requestId ${body.requestId}`);
350
+ return;
351
+ }
352
+ s.pending.delete(body.requestId);
353
+ pending.resolve(body.answer);
354
+ json(res, 200, { accepted: true });
355
+ return;
356
+ }
357
+ const cancel = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/cancel$/);
358
+ if (cancel && method === "POST") {
359
+ const s = sessions.get(decodeURIComponent(cancel[1]));
360
+ if (!s) {
361
+ jsonError(res, 404, `unknown session ${decodeURIComponent(cancel[1])}`);
362
+ return;
363
+ }
364
+ if (s.busy) s.busy.controller.abort();
365
+ failPending(s);
366
+ json(res, 200, { cancelled: true });
367
+ return;
368
+ }
369
+ jsonError(res, 404, `no route ${method} ${url.pathname}`);
370
+ }
371
+ async function runTurn(s, prompt, controller) {
372
+ let text = "";
373
+ let stopReason = "done";
374
+ const toolCalls = [];
375
+ const byId = /* @__PURE__ */ new Map();
376
+ let usage = { inputTokens: 0, outputTokens: 0 };
377
+ const map = (event) => {
378
+ switch (event.type) {
379
+ case "text":
380
+ text += event.delta;
381
+ broadcast(s, { type: "text", delta: event.delta });
382
+ break;
383
+ case "textReplace":
384
+ text = event.text;
385
+ break;
386
+ case "thinking":
387
+ broadcast(s, { type: "thinking", delta: event.delta });
388
+ break;
389
+ case "toolStart": {
390
+ const call = { name: event.tool, target: event.target };
391
+ byId.set(event.id, call);
392
+ toolCalls.push(call);
393
+ broadcast(s, { type: "tool_use", tool: event.tool, target: event.target });
394
+ break;
395
+ }
396
+ case "toolEnd": {
397
+ const call = byId.get(event.id);
398
+ if (call) {
399
+ call.output = event.output;
400
+ call.isError = event.isError;
401
+ }
402
+ broadcast(s, { type: "tool_result", tool: event.tool, output: event.output, isError: event.isError });
403
+ break;
404
+ }
405
+ case "usage":
406
+ usage = { inputTokens: event.inputTokens, outputTokens: event.outputTokens };
407
+ broadcast(s, { type: "usage", ...usage });
408
+ break;
409
+ case "stepLimit":
410
+ stopReason = "stepLimit";
411
+ break;
412
+ case "budgetExceeded":
413
+ stopReason = "budgetExceeded";
414
+ break;
415
+ case "aborted":
416
+ stopReason = "aborted";
417
+ break;
418
+ case "error":
419
+ broadcast(s, { type: "error", message: event.message });
420
+ break;
421
+ default:
422
+ break;
423
+ }
424
+ };
425
+ s.rt.agent.onEvent = map;
426
+ try {
427
+ await s.rt.agent.run(prompt, controller.signal);
428
+ } catch {
429
+ stopReason = controller.signal.aborted ? "aborted" : stopReason;
430
+ } finally {
431
+ s.rt.agent.onEvent = () => {
432
+ };
433
+ s.busy = null;
434
+ failPending(s);
435
+ broadcast(s, { type: "result", text, toolCalls, usage, stopReason });
436
+ }
437
+ }
438
+ function findSessionFile(id, forCwd) {
439
+ const hit = Session.list(home, forCwd).find(({ session }) => basename(session.file, ".jsonl") === id);
440
+ return hit ? { session: hit.session, cwd: forCwd } : void 0;
441
+ }
442
+ function buildRuntime(forCwd, permissionMode, model) {
443
+ return createRuntime({
444
+ cwd: forCwd,
445
+ args: { ...parseCliArgs([]), allowAll: permissionMode === "bypassPermissions", model },
446
+ env,
447
+ provider: opts.provider,
448
+ fetch: opts.fetch
449
+ });
450
+ }
451
+ function openSession(forCwd, permissionMode, model) {
452
+ return registerSession(buildRuntime(forCwd, permissionMode, model), forCwd, permissionMode);
453
+ }
454
+ function attachSession(id, forCwd) {
455
+ const found = findSessionFile(id, forCwd);
456
+ if (!found) return void 0;
457
+ const rt = buildRuntime(found.cwd, "ask");
458
+ rt.resume(found.session);
459
+ return registerSession(rt, found.cwd, "ask", id);
460
+ }
461
+ function registerSession(rt, forCwd, permissionMode, id) {
462
+ const sessionId = id ?? basename(rt.session.file, ".jsonl");
463
+ const s = {
464
+ id: sessionId,
465
+ cwd: forCwd,
466
+ rt,
467
+ permissionMode,
468
+ busy: null,
469
+ streams: /* @__PURE__ */ new Set(),
470
+ pending: /* @__PURE__ */ new Map()
471
+ };
472
+ if (permissionMode === "bypassPermissions") {
473
+ rt.agent.askPermission = async () => "yes";
474
+ } else {
475
+ rt.agent.askPermission = (req) => new Promise((resolve) => {
476
+ const requestId = randomBytes(6).toString("hex");
477
+ s.pending.set(requestId, { resolve, req });
478
+ broadcast(s, {
479
+ type: "approval_request",
480
+ requestId,
481
+ tool: req.tool,
482
+ kind: req.kind,
483
+ target: req.target,
484
+ preview: req.preview
485
+ });
486
+ });
487
+ }
488
+ sessions.set(sessionId, s);
489
+ return s;
490
+ }
491
+ function failPending(s) {
492
+ for (const [, p] of s.pending) p.resolve("no");
493
+ s.pending.clear();
494
+ }
495
+ await new Promise(
496
+ (resolve) => server.listen(opts.port ?? 8787, host === "localhost" ? "127.0.0.1" : host, resolve)
497
+ );
498
+ const address = server.address();
499
+ const port = typeof address === "object" && address ? address.port : 0;
500
+ return {
501
+ port,
502
+ token,
503
+ url: `http://127.0.0.1:${port}`,
504
+ close: () => new Promise((resolve) => {
505
+ for (const s of sessions.values()) failPending(s);
506
+ for (const ws of allSockets) ws.terminate();
507
+ allSockets.clear();
508
+ server.closeAllConnections();
509
+ server.close(() => resolve());
510
+ })
511
+ };
512
+ }
513
+ export {
514
+ startServe
515
+ };
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Session,
4
+ pruneSessions
5
+ } from "./chunk-F6ACN47V.js";
6
+ import "./chunk-R4BJBTWC.js";
7
+ import "./chunk-SNKVS26H.js";
8
+ import "./chunk-XLC6T4SX.js";
9
+ export {
10
+ Session,
11
+ pruneSessions
12
+ };