cli-funnel 0.1.0
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/data/models/agent.json +51 -0
- package/data/models/antigravity.json +103 -0
- package/data/models/claude.json +93 -0
- package/data/models/codex.json +217 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +164 -0
- package/dist/chunk-2FZ64BNF.js +1653 -0
- package/dist/chunk-GF7JASHO.js +317 -0
- package/dist/chunk-YP5V5YEE.js +15 -0
- package/dist/client/index.d.ts +52 -0
- package/dist/client/index.js +63 -0
- package/dist/funnel-CoOj5wr1.d.ts +264 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.js +9 -0
- package/package.json +57 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCESS_LEVELS,
|
|
3
|
+
FunnelError
|
|
4
|
+
} from "./chunk-YP5V5YEE.js";
|
|
5
|
+
|
|
6
|
+
// src/server/handler.ts
|
|
7
|
+
import { timingSafeEqual } from "crypto";
|
|
8
|
+
import { readdir } from "fs/promises";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
|
|
11
|
+
|
|
12
|
+
// src/server/sse.ts
|
|
13
|
+
var sseHeaders = {
|
|
14
|
+
"content-type": "text/event-stream",
|
|
15
|
+
"cache-control": "no-cache, no-transform",
|
|
16
|
+
connection: "keep-alive"
|
|
17
|
+
};
|
|
18
|
+
function json(body, status = 200) {
|
|
19
|
+
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
20
|
+
}
|
|
21
|
+
function sse(items, opts = {}) {
|
|
22
|
+
const enc = new TextEncoder();
|
|
23
|
+
const it = items[Symbol.asyncIterator]();
|
|
24
|
+
return new ReadableStream({
|
|
25
|
+
async pull(controller) {
|
|
26
|
+
try {
|
|
27
|
+
const { value, done } = await it.next();
|
|
28
|
+
if (done) {
|
|
29
|
+
if (opts.done) controller.enqueue(enc.encode(`data: ${opts.done}
|
|
30
|
+
|
|
31
|
+
`));
|
|
32
|
+
controller.close();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(value)}
|
|
36
|
+
|
|
37
|
+
`));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify({ type: "error", message: String(err) })}
|
|
40
|
+
|
|
41
|
+
`));
|
|
42
|
+
controller.close();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async cancel() {
|
|
46
|
+
await it.return?.();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/server/openai.ts
|
|
52
|
+
var text = (c) => typeof c === "string" ? c : (c ?? []).map((p) => p.type === "text" ? p.text ?? "" : "").join("");
|
|
53
|
+
function toPrompt(messages, resumed) {
|
|
54
|
+
if (resumed) return text([...messages].reverse().find((m) => m.role === "user")?.content ?? "");
|
|
55
|
+
if (messages.length === 1) return text(messages[0].content);
|
|
56
|
+
return messages.map((m) => `${m.role.toUpperCase()}: ${text(m.content)}`).join("\n\n");
|
|
57
|
+
}
|
|
58
|
+
var finish = (r) => r === "stop" ? "stop" : r === "cancelled" ? "stop" : r === "denied" ? "content_filter" : "stop";
|
|
59
|
+
var usageOut = (u) => u ? { prompt_tokens: u.inputTokens, completion_tokens: u.outputTokens, total_tokens: u.totalTokens } : void 0;
|
|
60
|
+
async function handleOpenAI(funnel, req, path, defaults = {}, cwdAllowed = () => true) {
|
|
61
|
+
if (req.method === "GET" && path === "/v1/models") {
|
|
62
|
+
const ids = Object.keys(funnel.providers);
|
|
63
|
+
const lists = await Promise.all(ids.map((id2) => funnel.models(id2).catch(() => [])));
|
|
64
|
+
const data = ids.flatMap((id2, i) => lists[i].map((m) => ({ id: `${id2}/${m.id}`, object: "model", owned_by: id2, name: m.name })));
|
|
65
|
+
return json({ object: "list", data });
|
|
66
|
+
}
|
|
67
|
+
if (req.method !== "POST" || path !== "/v1/chat/completions") return json({ error: { message: "Not found" } }, 404);
|
|
68
|
+
const body = await req.json();
|
|
69
|
+
const slash = body.model.indexOf("/");
|
|
70
|
+
const provider = body.model.slice(0, slash);
|
|
71
|
+
const model = body.model.slice(slash + 1);
|
|
72
|
+
if (slash < 0 || !funnel.providers[provider]) {
|
|
73
|
+
return json({ error: { message: 'model must look like "<provider>/<model>", for example "claude/claude-sonnet-5".' } }, 400);
|
|
74
|
+
}
|
|
75
|
+
const access = body.x_funnel?.access ?? defaults.access ?? "accept-edits";
|
|
76
|
+
if (!ACCESS_LEVELS.includes(access)) return json({ error: { message: `bad access "${access}"` } }, 400);
|
|
77
|
+
const cwd = body.x_funnel?.cwd ?? defaults.cwd ?? process.cwd();
|
|
78
|
+
if (body.x_funnel?.cwd && !cwdAllowed(cwd)) return json({ error: { message: "x_funnel.cwd is outside the allowed roots." } }, 403);
|
|
79
|
+
const input = {
|
|
80
|
+
selection: {
|
|
81
|
+
provider,
|
|
82
|
+
model,
|
|
83
|
+
effort: body.reasoning_effort,
|
|
84
|
+
fast: body.x_funnel?.fast,
|
|
85
|
+
contextWindow: body.x_funnel?.contextWindow,
|
|
86
|
+
cwd,
|
|
87
|
+
access
|
|
88
|
+
},
|
|
89
|
+
prompt: toPrompt(body.messages, !!body.x_funnel?.sessionId),
|
|
90
|
+
sessionId: body.x_funnel?.sessionId,
|
|
91
|
+
signal: req.signal,
|
|
92
|
+
onApproval: () => "deny"
|
|
93
|
+
};
|
|
94
|
+
const id = `chatcmpl-${Date.now().toString(36)}`;
|
|
95
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
96
|
+
const chunk = (delta, finish_reason = null, extra = {}) => ({
|
|
97
|
+
id,
|
|
98
|
+
object: "chat.completion.chunk",
|
|
99
|
+
created,
|
|
100
|
+
model: body.model,
|
|
101
|
+
choices: [{ index: 0, delta, finish_reason }],
|
|
102
|
+
...extra
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
if (!body.stream) {
|
|
106
|
+
const r = await funnel.run(input);
|
|
107
|
+
return json({
|
|
108
|
+
id,
|
|
109
|
+
object: "chat.completion",
|
|
110
|
+
created,
|
|
111
|
+
model: body.model,
|
|
112
|
+
choices: [{ index: 0, message: { role: "assistant", content: r.text }, finish_reason: finish(r.finishReason) }],
|
|
113
|
+
usage: usageOut(r.usage),
|
|
114
|
+
x_funnel: { sessionId: r.sessionId, toolCalls: r.toolCalls, deniedActions: r.deniedActions }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const stream = funnel.stream(input);
|
|
118
|
+
const chunks = (async function* () {
|
|
119
|
+
yield chunk({ role: "assistant" });
|
|
120
|
+
let usage;
|
|
121
|
+
let reason = "stop";
|
|
122
|
+
let sessionId;
|
|
123
|
+
for await (const e of stream) {
|
|
124
|
+
if (e.type === "text.delta") yield chunk({ content: e.text });
|
|
125
|
+
else if (e.type === "session") sessionId = e.sessionId;
|
|
126
|
+
else if (e.type === "usage") usage = e.usage;
|
|
127
|
+
else if (e.type === "done") reason = e.finishReason;
|
|
128
|
+
else if (e.type === "error") throw new Error(e.message);
|
|
129
|
+
}
|
|
130
|
+
yield chunk({}, finish(reason), { usage: usageOut(usage), x_funnel: { sessionId } });
|
|
131
|
+
})();
|
|
132
|
+
return new Response(sse(chunks, { done: "[DONE]" }), { headers: sseHeaders });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return json({ error: { message: err instanceof Error ? err.message : String(err), type: "cli_funnel_error" } }, 500);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/server/handler.ts
|
|
139
|
+
function createHandler(funnel, options = {}) {
|
|
140
|
+
const base = (options.basePath ?? "").replace(/\/$/, "");
|
|
141
|
+
const roots = (options.fsRoots ?? [homedir()]).map((r) => resolve(r));
|
|
142
|
+
const approvals = /* @__PURE__ */ new Map();
|
|
143
|
+
const earlyDecisions = /* @__PURE__ */ new Map();
|
|
144
|
+
const logins = /* @__PURE__ */ new Map();
|
|
145
|
+
let seq = 0;
|
|
146
|
+
const insideRoots = (p) => roots.some((r) => p === r || p.startsWith(r + sep));
|
|
147
|
+
const cwdAllowed = (cwd) => isAbsolute(cwd) && insideRoots(resolve(cwd));
|
|
148
|
+
const tokenOk = (header) => {
|
|
149
|
+
const a = Buffer.from(header ?? "");
|
|
150
|
+
const b = Buffer.from(`Bearer ${options.token}`);
|
|
151
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
152
|
+
};
|
|
153
|
+
async function run(req) {
|
|
154
|
+
const body = await req.json();
|
|
155
|
+
if (!cwdAllowed(body.selection?.cwd ?? "")) {
|
|
156
|
+
return json({ error: "cwd is outside the allowed roots. Set fsRoots on the handler." }, 403);
|
|
157
|
+
}
|
|
158
|
+
const runId = `run_${++seq}_${Date.now().toString(36)}`;
|
|
159
|
+
const timeoutMs = (options.approvalTimeoutSec ?? 300) * 1e3;
|
|
160
|
+
const input = {
|
|
161
|
+
selection: body.selection,
|
|
162
|
+
prompt: body.prompt,
|
|
163
|
+
sessionId: body.sessionId,
|
|
164
|
+
signal: req.signal,
|
|
165
|
+
onApproval: (request) => new Promise((resolveDecision) => {
|
|
166
|
+
const key = `${runId}:${request.id}`;
|
|
167
|
+
const early = earlyDecisions.get(key);
|
|
168
|
+
if (early) {
|
|
169
|
+
earlyDecisions.delete(key);
|
|
170
|
+
return resolveDecision(early);
|
|
171
|
+
}
|
|
172
|
+
const timer = setTimeout(() => finish2("deny"), timeoutMs);
|
|
173
|
+
const finish2 = (d) => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
approvals.delete(key);
|
|
176
|
+
resolveDecision(d);
|
|
177
|
+
};
|
|
178
|
+
approvals.set(key, { resolve: finish2 });
|
|
179
|
+
})
|
|
180
|
+
};
|
|
181
|
+
const stream = funnel.stream(input);
|
|
182
|
+
const events = (async function* () {
|
|
183
|
+
yield { type: "run", runId };
|
|
184
|
+
try {
|
|
185
|
+
for await (const e of stream) yield e;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
yield { type: "error", message: err instanceof Error ? err.message : String(err), code: err.code };
|
|
188
|
+
}
|
|
189
|
+
})();
|
|
190
|
+
return new Response(sse(events), { headers: sseHeaders });
|
|
191
|
+
}
|
|
192
|
+
async function login(id, req) {
|
|
193
|
+
const session = funnel.login(id, { signal: req.signal });
|
|
194
|
+
const loginId = `login_${++seq}_${Date.now().toString(36)}`;
|
|
195
|
+
logins.set(loginId, session);
|
|
196
|
+
const events = (async function* () {
|
|
197
|
+
yield { type: "login", loginId };
|
|
198
|
+
try {
|
|
199
|
+
yield* session;
|
|
200
|
+
} finally {
|
|
201
|
+
logins.delete(loginId);
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
return new Response(sse(events), { headers: sseHeaders });
|
|
205
|
+
}
|
|
206
|
+
async function listDirs(url) {
|
|
207
|
+
const raw = url.searchParams.get("path") || roots[0];
|
|
208
|
+
const path = resolve(isAbsolute(raw) ? raw : join(roots[0], raw));
|
|
209
|
+
if (!insideRoots(path)) return json({ error: "Path is outside the allowed roots." }, 403);
|
|
210
|
+
const entries = await readdir(path, { withFileTypes: true }).catch(() => void 0);
|
|
211
|
+
if (!entries) return json({ error: "Cannot read directory." }, 404);
|
|
212
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
213
|
+
const parent = dirname(path);
|
|
214
|
+
return json({ path, name: basename(path), parent: insideRoots(parent) && parent !== path ? parent : null, dirs });
|
|
215
|
+
}
|
|
216
|
+
return async function handle(req) {
|
|
217
|
+
const url = new URL(req.url);
|
|
218
|
+
let path = url.pathname;
|
|
219
|
+
if (base) {
|
|
220
|
+
if (!path.startsWith(base)) return json({ error: "Not found" }, 404);
|
|
221
|
+
path = path.slice(base.length) || "/";
|
|
222
|
+
}
|
|
223
|
+
if (options.token && !tokenOk(req.headers.get("authorization"))) {
|
|
224
|
+
return json({ error: "Unauthorized" }, 401);
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const seg = path.split("/").filter(Boolean);
|
|
228
|
+
const m = req.method;
|
|
229
|
+
if (path.startsWith("/v1/")) return await handleOpenAI(funnel, req, path, options.openai, cwdAllowed);
|
|
230
|
+
if (m === "GET" && path === "/providers") return json(await funnel.overview());
|
|
231
|
+
if (m === "GET" && path === "/fs") return await listDirs(url);
|
|
232
|
+
if (m === "POST" && path === "/run") return await run(req);
|
|
233
|
+
if (seg[0] === "providers" && seg[1]) {
|
|
234
|
+
const id = seg[1];
|
|
235
|
+
if (!funnel.providers[id]) return json({ error: `Unknown provider ${id}` }, 404);
|
|
236
|
+
if (m === "GET" && seg[2] === "models") return json(await funnel.models(id));
|
|
237
|
+
if (m === "GET" && seg[2] === "auth") return json(await funnel.authStatus(id));
|
|
238
|
+
if (m === "POST" && seg[2] === "login") return await login(id, req);
|
|
239
|
+
if (m === "POST" && seg[2] === "logout") {
|
|
240
|
+
await funnel.logout(id);
|
|
241
|
+
return json({ ok: true });
|
|
242
|
+
}
|
|
243
|
+
if (m === "POST" && seg[2] === "update") return json(await funnel.update(id));
|
|
244
|
+
}
|
|
245
|
+
if (m === "POST" && seg[0] === "logins" && seg[1]) {
|
|
246
|
+
const session = logins.get(seg[1]);
|
|
247
|
+
if (!session) return json({ error: "Unknown login" }, 404);
|
|
248
|
+
if (seg[2] === "code") {
|
|
249
|
+
const { code } = await req.json();
|
|
250
|
+
session.sendCode(code);
|
|
251
|
+
} else if (seg[2] === "cancel") session.cancel();
|
|
252
|
+
return json({ ok: true });
|
|
253
|
+
}
|
|
254
|
+
if (m === "POST" && seg[0] === "approvals" && seg[1] && seg[2]) {
|
|
255
|
+
const key = `${seg[1]}:${seg[2]}`;
|
|
256
|
+
const { decision } = await req.json();
|
|
257
|
+
const answer = decision === "allow" ? "allow" : "deny";
|
|
258
|
+
const pending = approvals.get(key);
|
|
259
|
+
if (pending) pending.resolve(answer);
|
|
260
|
+
else {
|
|
261
|
+
earlyDecisions.set(key, answer);
|
|
262
|
+
setTimeout(() => earlyDecisions.delete(key), 6e4).unref();
|
|
263
|
+
}
|
|
264
|
+
return json({ ok: true });
|
|
265
|
+
}
|
|
266
|
+
return json({ error: "Not found" }, 404);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
const status = err instanceof FunnelError ? err.code === "invalid-selection" ? 400 : 409 : 500;
|
|
269
|
+
return json({ error: err instanceof Error ? err.message : String(err), code: err.code }, status);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/server/node.ts
|
|
275
|
+
import { createServer } from "http";
|
|
276
|
+
import { Readable } from "stream";
|
|
277
|
+
function serveNode(handler, opts = {}) {
|
|
278
|
+
const host = opts.host ?? "127.0.0.1";
|
|
279
|
+
const server = createServer(async (nodeReq, nodeRes) => {
|
|
280
|
+
const ac = new AbortController();
|
|
281
|
+
nodeRes.on("close", () => ac.abort());
|
|
282
|
+
const url = `http://${nodeReq.headers.host ?? host}${nodeReq.url ?? "/"}`;
|
|
283
|
+
const hasBody = nodeReq.method !== "GET" && nodeReq.method !== "HEAD";
|
|
284
|
+
const req = new Request(url, {
|
|
285
|
+
method: nodeReq.method,
|
|
286
|
+
headers: nodeReq.headers,
|
|
287
|
+
body: hasBody ? Readable.toWeb(nodeReq) : void 0,
|
|
288
|
+
duplex: "half",
|
|
289
|
+
signal: ac.signal
|
|
290
|
+
});
|
|
291
|
+
const res = await handler(req);
|
|
292
|
+
nodeRes.writeHead(res.status, Object.fromEntries(res.headers));
|
|
293
|
+
if (!res.body) return nodeRes.end();
|
|
294
|
+
const reader = res.body.getReader();
|
|
295
|
+
try {
|
|
296
|
+
for (; ; ) {
|
|
297
|
+
const { done, value } = await reader.read();
|
|
298
|
+
if (done) break;
|
|
299
|
+
nodeRes.write(value);
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
nodeRes.end();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return new Promise((resolve2) => {
|
|
306
|
+
server.listen(opts.port ?? 4747, host, () => {
|
|
307
|
+
const addr = server.address();
|
|
308
|
+
const port = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
309
|
+
resolve2({ server, url: `http://${host}:${port}` });
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export {
|
|
315
|
+
createHandler,
|
|
316
|
+
serveNode
|
|
317
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var ACCESS_LEVELS = ["supervised", "accept-edits", "auto", "full"];
|
|
3
|
+
var FunnelError = class extends Error {
|
|
4
|
+
constructor(message, code) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "FunnelError";
|
|
8
|
+
}
|
|
9
|
+
code;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
ACCESS_LEVELS,
|
|
14
|
+
FunnelError
|
|
15
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { P as ProviderOverview, a as ProviderId, M as ModelInfo, A as AuthStatus, U as UpdateResult, L as LoginEvent, R as RunInput, S as Selection, F as FunnelEvent, b as ApprovalDecision } from '../funnel-CoOj5wr1.js';
|
|
2
|
+
export { c as ACCESS_LEVELS, d as AccessLevel, e as ApiProviderId, f as ApprovalRequest, C as Capabilities, g as CliProviderId, E as EffortOption, h as FinishReason, i as FunnelError, I as Installation, j as LoginSession, k as Provider, l as RunResult, m as Usage } from '../funnel-CoOj5wr1.js';
|
|
3
|
+
|
|
4
|
+
interface ClientOptions {
|
|
5
|
+
/** Where the handler is mounted, for example "/api/funnel" or "http://localhost:4747". */
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
type RunEvent = FunnelEvent | {
|
|
11
|
+
type: "run";
|
|
12
|
+
runId: string;
|
|
13
|
+
};
|
|
14
|
+
type LoginStreamEvent = LoginEvent | {
|
|
15
|
+
type: "login";
|
|
16
|
+
loginId: string;
|
|
17
|
+
};
|
|
18
|
+
interface DirListing {
|
|
19
|
+
path: string;
|
|
20
|
+
name: string;
|
|
21
|
+
parent: string | null;
|
|
22
|
+
dirs: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Works in browsers, Node and edge runtimes. Talks to `createHandler` from "cli-funnel/server". */
|
|
25
|
+
declare function createClient(options: ClientOptions): {
|
|
26
|
+
providers: () => Promise<ProviderOverview[]>;
|
|
27
|
+
models: (id: ProviderId) => Promise<ModelInfo[]>;
|
|
28
|
+
authStatus: (id: ProviderId) => Promise<AuthStatus>;
|
|
29
|
+
logout: (id: ProviderId) => Promise<{
|
|
30
|
+
ok: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
update: (id: ProviderId) => Promise<UpdateResult>;
|
|
33
|
+
listDirs: (path?: string) => Promise<DirListing>;
|
|
34
|
+
/** Streams login events. The first event is `{ type: "login", loginId }`. */
|
|
35
|
+
login: (id: ProviderId, signal?: AbortSignal) => AsyncGenerator<LoginStreamEvent, any, any>;
|
|
36
|
+
sendLoginCode: (loginId: string, code: string) => Promise<{
|
|
37
|
+
ok: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
cancelLogin: (loginId: string) => Promise<{
|
|
40
|
+
ok: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
/** Streams run events. The first event is `{ type: "run", runId }`, needed to answer approvals. */
|
|
43
|
+
run: (input: Pick<RunInput, "prompt" | "sessionId"> & {
|
|
44
|
+
selection: Selection;
|
|
45
|
+
}, signal?: AbortSignal) => AsyncGenerator<RunEvent, any, any>;
|
|
46
|
+
approve: (runId: string, approvalId: string, decision: ApprovalDecision) => Promise<{
|
|
47
|
+
ok: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
type FunnelClient = ReturnType<typeof createClient>;
|
|
51
|
+
|
|
52
|
+
export { ApprovalDecision, AuthStatus, type ClientOptions, type DirListing, type FunnelClient, FunnelEvent, LoginEvent, type LoginStreamEvent, ModelInfo, ProviderId, ProviderOverview, type RunEvent, RunInput, Selection, UpdateResult, createClient };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCESS_LEVELS,
|
|
3
|
+
FunnelError
|
|
4
|
+
} from "../chunk-YP5V5YEE.js";
|
|
5
|
+
|
|
6
|
+
// src/client/index.ts
|
|
7
|
+
function createClient(options) {
|
|
8
|
+
const doFetch = options.fetch ?? fetch;
|
|
9
|
+
const base = options.baseUrl.replace(/\/$/, "");
|
|
10
|
+
const headers = (extra) => ({
|
|
11
|
+
...options.token ? { authorization: `Bearer ${options.token}` } : {},
|
|
12
|
+
...extra
|
|
13
|
+
});
|
|
14
|
+
async function request(path, init) {
|
|
15
|
+
const res = await doFetch(base + path, { ...init, headers: headers({ "content-type": "application/json" }) });
|
|
16
|
+
const body = await res.json().catch(() => ({}));
|
|
17
|
+
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
18
|
+
return body;
|
|
19
|
+
}
|
|
20
|
+
async function* events(path, body, signal) {
|
|
21
|
+
const res = await doFetch(base + path, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
body: JSON.stringify(body),
|
|
24
|
+
signal,
|
|
25
|
+
headers: headers({ "content-type": "application/json" })
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok || !res.body) throw new Error((await res.json().catch(() => ({}))).error ?? `HTTP ${res.status}`);
|
|
28
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
29
|
+
let buffer = "";
|
|
30
|
+
for (; ; ) {
|
|
31
|
+
const { done, value } = await reader.read();
|
|
32
|
+
if (done) return;
|
|
33
|
+
buffer += value;
|
|
34
|
+
let i;
|
|
35
|
+
while ((i = buffer.indexOf("\n\n")) >= 0) {
|
|
36
|
+
const line = buffer.slice(0, i).trim();
|
|
37
|
+
buffer = buffer.slice(i + 2);
|
|
38
|
+
if (line.startsWith("data:")) yield JSON.parse(line.slice(5).trim());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const post = (path, body = {}) => request(path, { method: "POST", body: JSON.stringify(body) });
|
|
43
|
+
return {
|
|
44
|
+
providers: () => request("/providers"),
|
|
45
|
+
models: (id) => request(`/providers/${id}/models`),
|
|
46
|
+
authStatus: (id) => request(`/providers/${id}/auth`),
|
|
47
|
+
logout: (id) => post(`/providers/${id}/logout`),
|
|
48
|
+
update: (id) => request(`/providers/${id}/update`, { method: "POST", body: "{}" }),
|
|
49
|
+
listDirs: (path) => request(`/fs${path ? `?path=${encodeURIComponent(path)}` : ""}`),
|
|
50
|
+
/** Streams login events. The first event is `{ type: "login", loginId }`. */
|
|
51
|
+
login: (id, signal) => events(`/providers/${id}/login`, {}, signal),
|
|
52
|
+
sendLoginCode: (loginId, code) => post(`/logins/${loginId}/code`, { code }),
|
|
53
|
+
cancelLogin: (loginId) => post(`/logins/${loginId}/cancel`),
|
|
54
|
+
/** Streams run events. The first event is `{ type: "run", runId }`, needed to answer approvals. */
|
|
55
|
+
run: (input, signal) => events("/run", input, signal),
|
|
56
|
+
approve: (runId, approvalId, decision) => post(`/approvals/${runId}/${approvalId}`, { decision })
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
ACCESS_LEVELS,
|
|
61
|
+
FunnelError,
|
|
62
|
+
createClient
|
|
63
|
+
};
|