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,1653 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCESS_LEVELS,
|
|
3
|
+
FunnelError
|
|
4
|
+
} from "./chunk-YP5V5YEE.js";
|
|
5
|
+
|
|
6
|
+
// src/catalog/manifest.ts
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var bundledDir = [new URL("../../data/models/", import.meta.url), new URL("../data/models/", import.meta.url)].find(
|
|
11
|
+
(u) => existsSync(fileURLToPath(u))
|
|
12
|
+
) ?? new URL("../../data/models/", import.meta.url);
|
|
13
|
+
var PROVIDER_IDS = ["claude", "codex", "agent", "antigravity"];
|
|
14
|
+
var bundled;
|
|
15
|
+
var remote;
|
|
16
|
+
async function loadBundledManifest() {
|
|
17
|
+
if (bundled) return bundled;
|
|
18
|
+
const providers = {};
|
|
19
|
+
for (const id of PROVIDER_IDS) {
|
|
20
|
+
providers[id] = JSON.parse(await readFile(fileURLToPath(new URL(`${id}.json`, bundledDir)), "utf8"));
|
|
21
|
+
}
|
|
22
|
+
bundled = { schemaVersion: 1, updated: "bundled", providers };
|
|
23
|
+
return bundled;
|
|
24
|
+
}
|
|
25
|
+
async function loadManifest(url) {
|
|
26
|
+
if (!url) return loadBundledManifest();
|
|
27
|
+
if (remote && remote.url === url && Date.now() - remote.at < 36e5) return remote.data;
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(4e3) });
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
if (data.schemaVersion === 1) {
|
|
33
|
+
remote = { url, at: Date.now(), data };
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return loadBundledManifest();
|
|
40
|
+
}
|
|
41
|
+
function fromManifest(manifest, provider, cliVersion, compare) {
|
|
42
|
+
return (manifest.providers[provider] ?? []).filter((m) => !m.minCliVersion || !cliVersion || !compare || compare(cliVersion, m.minCliVersion) >= 0).map(({ minCliVersion: _min, ...m }) => ({ ...m, provider, source: "manifest" }));
|
|
43
|
+
}
|
|
44
|
+
function mergeModels(live, fromFile) {
|
|
45
|
+
const seen = new Set(live.map((m) => m.id));
|
|
46
|
+
return [...live, ...fromFile.filter((m) => !seen.has(m.id))];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/selection.ts
|
|
50
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
51
|
+
import { isAbsolute } from "path";
|
|
52
|
+
function validateSelection(selection, provider, models2) {
|
|
53
|
+
const bad = (msg) => {
|
|
54
|
+
throw new FunnelError(msg, "invalid-selection");
|
|
55
|
+
};
|
|
56
|
+
if (selection.provider !== provider.id) bad(`Selection is for ${selection.provider}, not ${provider.id}.`);
|
|
57
|
+
if (!ACCESS_LEVELS.includes(selection.access)) bad(`Unknown access level "${selection.access}".`);
|
|
58
|
+
const hasMachineAccess = provider.capabilities.access.length > 0;
|
|
59
|
+
if (hasMachineAccess && !provider.capabilities.access.includes(selection.access)) {
|
|
60
|
+
bad(`${provider.displayName} cannot enforce access "${selection.access}". Supported: ${provider.capabilities.access.join(", ")}.`);
|
|
61
|
+
}
|
|
62
|
+
if (hasMachineAccess && (!isAbsolute(selection.cwd) || !existsSync2(selection.cwd) || !statSync(selection.cwd).isDirectory())) {
|
|
63
|
+
bad(`cwd must be an existing absolute directory: ${selection.cwd}`);
|
|
64
|
+
}
|
|
65
|
+
const { capabilities: cap } = provider;
|
|
66
|
+
if (selection.effort && !cap.effort) bad(`${provider.displayName} has no effort setting.`);
|
|
67
|
+
if (selection.fast && !cap.fast) bad(`${provider.displayName} has no fast mode.`);
|
|
68
|
+
if (selection.contextWindow && !cap.contextWindow) bad(`${provider.displayName} has no context window setting.`);
|
|
69
|
+
if (!selection.model) bad("model is required.");
|
|
70
|
+
const model = models2?.find((m) => m.id === selection.model);
|
|
71
|
+
if (models2 && !model) bad(`Unknown model "${selection.model}" for ${provider.displayName}. Call funnel.models() for the list.`);
|
|
72
|
+
if (model) {
|
|
73
|
+
if (selection.effort && !model.efforts.some((e) => e.id === selection.effort)) {
|
|
74
|
+
bad(`${model.id} does not support effort "${selection.effort}".`);
|
|
75
|
+
}
|
|
76
|
+
if (selection.fast && !model.fast) bad(`${model.id} has no fast tier.`);
|
|
77
|
+
if (selection.contextWindow && !model.contextWindows.includes(selection.contextWindow)) {
|
|
78
|
+
bad(`${model.id} does not offer a ${selection.contextWindow} token context window.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/providers/api.ts
|
|
84
|
+
import { randomUUID } from "crypto";
|
|
85
|
+
|
|
86
|
+
// src/util/sse.ts
|
|
87
|
+
async function* readSSE(res) {
|
|
88
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
89
|
+
let buffer = "";
|
|
90
|
+
for (; ; ) {
|
|
91
|
+
const { done, value } = await reader.read();
|
|
92
|
+
if (done) return;
|
|
93
|
+
buffer += value;
|
|
94
|
+
let i;
|
|
95
|
+
while ((i = buffer.search(/\r?\n\r?\n/)) >= 0) {
|
|
96
|
+
const block = buffer.slice(0, i);
|
|
97
|
+
buffer = buffer.slice(i).replace(/^\r?\n\r?\n/, "");
|
|
98
|
+
let event;
|
|
99
|
+
const data = [];
|
|
100
|
+
for (const line of block.split(/\r?\n/)) {
|
|
101
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
102
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).trim());
|
|
103
|
+
}
|
|
104
|
+
if (data.length) yield { event, data: data.join("\n") };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/providers/api.ts
|
|
110
|
+
var NO_EFFORT = { efforts: [], contextWindows: [], fast: false, source: "cli" };
|
|
111
|
+
var anthropic = {
|
|
112
|
+
id: "anthropic-api",
|
|
113
|
+
displayName: "Anthropic API",
|
|
114
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
115
|
+
keyHint: "console.anthropic.com",
|
|
116
|
+
async listModels(key) {
|
|
117
|
+
const res = await fetch("https://api.anthropic.com/v1/models?limit=1000", {
|
|
118
|
+
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" }
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) throw new FunnelError(`Anthropic API returned ${res.status}.`, "cli-failed");
|
|
121
|
+
const body = await res.json();
|
|
122
|
+
return body.data.map((m) => ({ id: m.id, name: m.display_name ?? m.id, provider: "anthropic-api", ...NO_EFFORT }));
|
|
123
|
+
},
|
|
124
|
+
async *stream(key, input, messages) {
|
|
125
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
126
|
+
method: "POST",
|
|
127
|
+
signal: input.signal,
|
|
128
|
+
headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json" },
|
|
129
|
+
body: JSON.stringify({ model: input.selection.model, max_tokens: 8192, stream: true, messages })
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) return yield { type: "error", message: `Anthropic API ${res.status}: ${await res.text()}`, code: "cli-failed" };
|
|
132
|
+
let inTok = 0;
|
|
133
|
+
let outTok = 0;
|
|
134
|
+
let cached = 0;
|
|
135
|
+
let stop = "stop";
|
|
136
|
+
for await (const { data } of readSSE(res)) {
|
|
137
|
+
const e = JSON.parse(data);
|
|
138
|
+
if (e.type === "message_start") {
|
|
139
|
+
inTok = e.message.usage?.input_tokens ?? 0;
|
|
140
|
+
cached = e.message.usage?.cache_read_input_tokens ?? 0;
|
|
141
|
+
} else if (e.type === "content_block_delta" && e.delta?.type === "text_delta") yield { type: "text.delta", text: e.delta.text };
|
|
142
|
+
else if (e.type === "message_delta") {
|
|
143
|
+
outTok = e.usage?.output_tokens ?? outTok;
|
|
144
|
+
stop = e.delta?.stop_reason ?? stop;
|
|
145
|
+
} else if (e.type === "error") return yield { type: "error", message: e.error?.message ?? "API error" };
|
|
146
|
+
}
|
|
147
|
+
yield { type: "usage", usage: { inputTokens: inTok, outputTokens: outTok, cachedInputTokens: cached, totalTokens: inTok + outTok } };
|
|
148
|
+
yield { type: "done", text: "", finishReason: stop === "refusal" ? "denied" : "stop" };
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var openai = {
|
|
152
|
+
id: "openai-api",
|
|
153
|
+
displayName: "OpenAI API",
|
|
154
|
+
envVar: "OPENAI_API_KEY",
|
|
155
|
+
keyHint: "platform.openai.com",
|
|
156
|
+
async listModels(key) {
|
|
157
|
+
const res = await fetch("https://api.openai.com/v1/models", { headers: { authorization: `Bearer ${key}` } });
|
|
158
|
+
if (!res.ok) throw new FunnelError(`OpenAI API returned ${res.status}.`, "cli-failed");
|
|
159
|
+
const body = await res.json();
|
|
160
|
+
return body.data.filter((m) => /^(gpt-|o\d|chatgpt)/.test(m.id) && !/(audio|realtime|tts|transcribe|image|search|embedding|moderation|instruct)/.test(m.id)).map((m) => ({ id: m.id, name: m.id, provider: "openai-api", ...NO_EFFORT })).sort((a, b) => a.id.localeCompare(b.id));
|
|
161
|
+
},
|
|
162
|
+
async *stream(key, input, messages) {
|
|
163
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
164
|
+
method: "POST",
|
|
165
|
+
signal: input.signal,
|
|
166
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
167
|
+
body: JSON.stringify({ model: input.selection.model, messages, stream: true, stream_options: { include_usage: true } })
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok) return yield { type: "error", message: `OpenAI API ${res.status}: ${await res.text()}`, code: "cli-failed" };
|
|
170
|
+
let usage;
|
|
171
|
+
let stop = "stop";
|
|
172
|
+
for await (const { data } of readSSE(res)) {
|
|
173
|
+
if (data === "[DONE]") break;
|
|
174
|
+
const e = JSON.parse(data);
|
|
175
|
+
const choice = e.choices?.[0];
|
|
176
|
+
if (choice?.delta?.content) yield { type: "text.delta", text: choice.delta.content };
|
|
177
|
+
if (choice?.finish_reason) stop = choice.finish_reason;
|
|
178
|
+
if (e.usage) usage = e.usage;
|
|
179
|
+
}
|
|
180
|
+
if (usage) yield { type: "usage", usage: { inputTokens: usage.prompt_tokens, outputTokens: usage.completion_tokens, totalTokens: usage.total_tokens } };
|
|
181
|
+
yield { type: "done", text: "", finishReason: stop === "content_filter" ? "denied" : "stop" };
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var histories = /* @__PURE__ */ new Map();
|
|
185
|
+
function makeApiProvider(spec, getKey) {
|
|
186
|
+
const key = () => {
|
|
187
|
+
const k = getKey() ?? process.env[spec.envVar];
|
|
188
|
+
if (!k) throw new FunnelError(`Set ${spec.envVar} (get a key at ${spec.keyHint}) or pass it to createFunnel({ apiKeys }).`, "not-logged-in");
|
|
189
|
+
return k;
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
id: spec.id,
|
|
193
|
+
displayName: spec.displayName,
|
|
194
|
+
binary: "fetch",
|
|
195
|
+
capabilities: { access: [], effort: false, contextWindow: false, fast: false, resume: true, approvals: false },
|
|
196
|
+
detect: async () => ({ installed: true, testedRange: { min: "0" }, withinTestedRange: true }),
|
|
197
|
+
authStatus: async () => ({ loggedIn: !!(getKey() ?? process.env[spec.envVar]), method: "API key" }),
|
|
198
|
+
login() {
|
|
199
|
+
return {
|
|
200
|
+
async *[Symbol.asyncIterator]() {
|
|
201
|
+
yield { type: "error", message: `${spec.displayName} uses an API key. Set ${spec.envVar}.` };
|
|
202
|
+
},
|
|
203
|
+
sendCode() {
|
|
204
|
+
},
|
|
205
|
+
cancel() {
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
logout: async () => {
|
|
210
|
+
throw new FunnelError(`Remove ${spec.envVar} from your environment to sign out.`, "unsupported");
|
|
211
|
+
},
|
|
212
|
+
update: async () => ({ changed: false, output: "API providers have nothing to update." }),
|
|
213
|
+
models: async () => spec.listModels(key()),
|
|
214
|
+
async *run(input) {
|
|
215
|
+
const sessionId = input.sessionId ?? randomUUID();
|
|
216
|
+
const messages = [...histories.get(sessionId) ?? [], { role: "user", content: input.prompt }];
|
|
217
|
+
yield { type: "session", sessionId, model: input.selection.model };
|
|
218
|
+
let text = "";
|
|
219
|
+
for await (const e of spec.stream(key(), input, messages)) {
|
|
220
|
+
if (e.type === "text.delta") text += e.text;
|
|
221
|
+
yield e;
|
|
222
|
+
}
|
|
223
|
+
histories.set(sessionId, [...messages, { role: "assistant", content: text }]);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
var createApiProviders = (keys = {}) => ({
|
|
228
|
+
"anthropic-api": makeApiProvider(anthropic, () => keys.anthropic),
|
|
229
|
+
"openai-api": makeApiProvider(openai, () => keys.openai)
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// src/util/channel.ts
|
|
233
|
+
var Channel = class {
|
|
234
|
+
queue = [];
|
|
235
|
+
waiting = [];
|
|
236
|
+
closed = false;
|
|
237
|
+
failure;
|
|
238
|
+
push(value) {
|
|
239
|
+
if (this.closed) return;
|
|
240
|
+
const waiter = this.waiting.shift();
|
|
241
|
+
if (waiter) waiter({ value, done: false });
|
|
242
|
+
else this.queue.push(value);
|
|
243
|
+
}
|
|
244
|
+
end() {
|
|
245
|
+
this.closed = true;
|
|
246
|
+
for (const w of this.waiting.splice(0)) w({ value: void 0, done: true });
|
|
247
|
+
}
|
|
248
|
+
fail(error) {
|
|
249
|
+
this.failure = error;
|
|
250
|
+
this.end();
|
|
251
|
+
}
|
|
252
|
+
[Symbol.asyncIterator]() {
|
|
253
|
+
return {
|
|
254
|
+
next: async () => {
|
|
255
|
+
if (this.queue.length) return { value: this.queue.shift(), done: false };
|
|
256
|
+
if (this.closed) {
|
|
257
|
+
if (this.failure) throw this.failure;
|
|
258
|
+
return { value: void 0, done: true };
|
|
259
|
+
}
|
|
260
|
+
return new Promise((resolve) => this.waiting.push(resolve));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// src/providers/base.ts
|
|
267
|
+
import { spawn as spawn2 } from "child_process";
|
|
268
|
+
|
|
269
|
+
// src/util/process.ts
|
|
270
|
+
import { spawn } from "child_process";
|
|
271
|
+
import { existsSync as existsSync3, accessSync, constants } from "fs";
|
|
272
|
+
import { homedir } from "os";
|
|
273
|
+
import { delimiter, join } from "path";
|
|
274
|
+
var EXTRA_DIRS = [
|
|
275
|
+
join(homedir(), ".local", "bin"),
|
|
276
|
+
join(homedir(), ".bun", "bin"),
|
|
277
|
+
join(homedir(), ".cursor", "bin"),
|
|
278
|
+
join(homedir(), ".npm-global", "bin"),
|
|
279
|
+
"/opt/homebrew/bin",
|
|
280
|
+
"/usr/local/bin"
|
|
281
|
+
];
|
|
282
|
+
function resolveBinary(name, envVar) {
|
|
283
|
+
const override = envVar ? process.env[envVar] : void 0;
|
|
284
|
+
if (override && existsSync3(override)) return override;
|
|
285
|
+
const dirs = [...(process.env.PATH ?? "").split(delimiter), ...EXTRA_DIRS];
|
|
286
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
287
|
+
for (const dir of dirs) {
|
|
288
|
+
for (const ext of exts) {
|
|
289
|
+
const candidate = join(dir, name + ext);
|
|
290
|
+
try {
|
|
291
|
+
accessSync(candidate, constants.X_OK);
|
|
292
|
+
return candidate;
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
function exec(file, args, opts = {}) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const child = spawn(file, args, {
|
|
302
|
+
cwd: opts.cwd,
|
|
303
|
+
env: { ...process.env, ...opts.env },
|
|
304
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
305
|
+
signal: opts.signal
|
|
306
|
+
});
|
|
307
|
+
let stdout = "";
|
|
308
|
+
let stderr = "";
|
|
309
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGTERM"), opts.timeoutMs) : void 0;
|
|
310
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
311
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
312
|
+
child.on("error", (e) => {
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
reject(e);
|
|
315
|
+
});
|
|
316
|
+
child.on("close", (code) => {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
resolve({ stdout, stderr, code });
|
|
319
|
+
});
|
|
320
|
+
child.stdin.on("error", () => {
|
|
321
|
+
});
|
|
322
|
+
child.stdin.end(opts.input ?? "");
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function spawnStream(file, args, opts = {}) {
|
|
326
|
+
const child = spawn(file, args, {
|
|
327
|
+
cwd: opts.cwd,
|
|
328
|
+
env: { ...process.env, ...opts.env },
|
|
329
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
330
|
+
});
|
|
331
|
+
const channel = new Channel();
|
|
332
|
+
let buffer = "";
|
|
333
|
+
let stderr = "";
|
|
334
|
+
const flush = (line) => {
|
|
335
|
+
const trimmed = line.trim();
|
|
336
|
+
if (!trimmed) return;
|
|
337
|
+
try {
|
|
338
|
+
channel.push(JSON.parse(trimmed));
|
|
339
|
+
} catch {
|
|
340
|
+
channel.push({ __raw: trimmed });
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
child.stdout.setEncoding("utf8");
|
|
344
|
+
child.stdout.on("data", (chunk) => {
|
|
345
|
+
buffer += chunk;
|
|
346
|
+
let i;
|
|
347
|
+
while ((i = buffer.indexOf("\n")) >= 0) {
|
|
348
|
+
flush(buffer.slice(0, i));
|
|
349
|
+
buffer = buffer.slice(i + 1);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
child.stderr.setEncoding("utf8");
|
|
353
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
354
|
+
child.stdin.on("error", () => {
|
|
355
|
+
});
|
|
356
|
+
const exited = new Promise((resolve) => {
|
|
357
|
+
child.on("error", (e) => {
|
|
358
|
+
channel.fail(e);
|
|
359
|
+
resolve(null);
|
|
360
|
+
});
|
|
361
|
+
child.on("close", (code) => {
|
|
362
|
+
flush(buffer);
|
|
363
|
+
buffer = "";
|
|
364
|
+
channel.end();
|
|
365
|
+
resolve(code);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
const onAbort = () => child.kill("SIGTERM");
|
|
369
|
+
if (opts.signal) {
|
|
370
|
+
if (opts.signal.aborted) onAbort();
|
|
371
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
372
|
+
}
|
|
373
|
+
if (opts.input !== void 0) child.stdin.write(opts.input);
|
|
374
|
+
if (opts.closeStdin ?? opts.input !== void 0) child.stdin.end();
|
|
375
|
+
return {
|
|
376
|
+
child,
|
|
377
|
+
lines: channel,
|
|
378
|
+
stderr: () => stderr,
|
|
379
|
+
exited,
|
|
380
|
+
write: (line) => child.stdin.write(line.endsWith("\n") ? line : line + "\n"),
|
|
381
|
+
closeStdin: () => child.stdin.end()
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function firstUrl(text) {
|
|
385
|
+
return text.match(/https?:\/\/[^\s"'<>)\]]+/)?.[0];
|
|
386
|
+
}
|
|
387
|
+
function parseVersion(text) {
|
|
388
|
+
return text.match(/\d+\.\d+\.\d+(?:[-.][\w.]+)?/)?.[0] ?? text.match(/\d{4}\.\d{2}\.\d{2}[-\w]*/)?.[0];
|
|
389
|
+
}
|
|
390
|
+
function compareVersions(a, b) {
|
|
391
|
+
const pa = a.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
392
|
+
const pb = b.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
393
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
394
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
395
|
+
if (d) return d;
|
|
396
|
+
}
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/providers/base.ts
|
|
401
|
+
function binaryEnvVar(binary) {
|
|
402
|
+
return `CLI_FUNNEL_${binary.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BIN`;
|
|
403
|
+
}
|
|
404
|
+
function findBinary(binary) {
|
|
405
|
+
return resolveBinary(binary, binaryEnvVar(binary));
|
|
406
|
+
}
|
|
407
|
+
async function detectInstallation(binary, testedRange, versionArgs = ["--version"]) {
|
|
408
|
+
const path = findBinary(binary);
|
|
409
|
+
if (!path) return { installed: false, testedRange };
|
|
410
|
+
const res = await exec(path, versionArgs, { timeoutMs: 1e4 }).catch(() => void 0);
|
|
411
|
+
const version = res ? parseVersion(res.stdout + res.stderr) : void 0;
|
|
412
|
+
const withinTestedRange = version === void 0 ? void 0 : compareVersions(version, testedRange.min) >= 0 && (!testedRange.maxExclusive || compareVersions(version, testedRange.maxExclusive) < 0);
|
|
413
|
+
return { installed: true, path, version, testedRange, withinTestedRange };
|
|
414
|
+
}
|
|
415
|
+
function spawnLogin(binary, args, readStatus, opts = {}) {
|
|
416
|
+
const channel = new Channel();
|
|
417
|
+
const path = findBinary(binary);
|
|
418
|
+
let child;
|
|
419
|
+
if (!path) {
|
|
420
|
+
channel.push({ type: "error", message: `${binary} is not installed.` });
|
|
421
|
+
channel.end();
|
|
422
|
+
} else {
|
|
423
|
+
child = spawn2(path, args, { env: { ...process.env, ...opts.env }, signal: opts.signal, stdio: ["pipe", "pipe", "pipe"] });
|
|
424
|
+
child.stdin?.on("error", () => {
|
|
425
|
+
});
|
|
426
|
+
const seen = /* @__PURE__ */ new Set();
|
|
427
|
+
const scan = (text) => {
|
|
428
|
+
channel.push({ type: "log", text });
|
|
429
|
+
const url = firstUrl(text);
|
|
430
|
+
if (url && !seen.has(url)) {
|
|
431
|
+
seen.add(url);
|
|
432
|
+
channel.push({ type: "open-url", url });
|
|
433
|
+
}
|
|
434
|
+
if (/paste|enter (the )?code|verification code/i.test(text)) {
|
|
435
|
+
channel.push({ type: "code-prompt", message: text.trim() });
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
child.stdout?.on("data", (d) => scan(String(d)));
|
|
439
|
+
child.stderr?.on("data", (d) => scan(String(d)));
|
|
440
|
+
child.on("close", async (code) => {
|
|
441
|
+
const status = await readStatus().catch(() => ({ loggedIn: false }));
|
|
442
|
+
if (status.loggedIn) channel.push({ type: "done", status });
|
|
443
|
+
else channel.push({ type: "error", message: `Login did not complete (exit ${code}).` });
|
|
444
|
+
channel.end();
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
[Symbol.asyncIterator]: () => channel[Symbol.asyncIterator](),
|
|
449
|
+
sendCode: (code) => child?.stdin?.write(code + "\n"),
|
|
450
|
+
cancel: () => child?.kill("SIGTERM")
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async function runUpdate(binary, updateArgs, versionArgs = ["--version"]) {
|
|
454
|
+
const path = findBinary(binary);
|
|
455
|
+
if (!path) return { changed: false, output: `${binary} is not installed.` };
|
|
456
|
+
const read = async () => {
|
|
457
|
+
const r = await exec(path, versionArgs, { timeoutMs: 1e4 }).catch(() => void 0);
|
|
458
|
+
return r ? parseVersion(r.stdout + r.stderr) : void 0;
|
|
459
|
+
};
|
|
460
|
+
const from = await read();
|
|
461
|
+
const res = await exec(path, updateArgs, { timeoutMs: 6e5 });
|
|
462
|
+
const to = await read();
|
|
463
|
+
return { from, to, changed: !!from && !!to && from !== to, output: (res.stdout + res.stderr).trim() };
|
|
464
|
+
}
|
|
465
|
+
async function runLogout(binary, args) {
|
|
466
|
+
const path = findBinary(binary);
|
|
467
|
+
if (path) await exec(path, args, { timeoutMs: 3e4 });
|
|
468
|
+
}
|
|
469
|
+
async function collect(events, input) {
|
|
470
|
+
const result = {
|
|
471
|
+
text: "",
|
|
472
|
+
provider: input.selection.provider,
|
|
473
|
+
model: input.selection.model,
|
|
474
|
+
finishReason: "stop",
|
|
475
|
+
toolCalls: [],
|
|
476
|
+
deniedActions: []
|
|
477
|
+
};
|
|
478
|
+
const tools = /* @__PURE__ */ new Map();
|
|
479
|
+
for await (const e of events) {
|
|
480
|
+
switch (e.type) {
|
|
481
|
+
case "session":
|
|
482
|
+
result.sessionId = e.sessionId;
|
|
483
|
+
if (e.model) result.model = e.model;
|
|
484
|
+
break;
|
|
485
|
+
case "text.delta":
|
|
486
|
+
result.text += e.text;
|
|
487
|
+
break;
|
|
488
|
+
case "tool.start": {
|
|
489
|
+
const call = { id: e.id, name: e.name, input: e.input };
|
|
490
|
+
tools.set(e.id, call);
|
|
491
|
+
result.toolCalls.push(call);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case "tool.end": {
|
|
495
|
+
const call = tools.get(e.id);
|
|
496
|
+
if (call && e.error) call.error = e.error;
|
|
497
|
+
if (e.error?.startsWith("denied:")) result.deniedActions.push(call?.name ?? e.error.slice(7).trim());
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "usage":
|
|
501
|
+
result.usage = e.usage;
|
|
502
|
+
break;
|
|
503
|
+
case "done":
|
|
504
|
+
if (e.text) result.text = e.text;
|
|
505
|
+
result.finishReason = e.finishReason;
|
|
506
|
+
break;
|
|
507
|
+
case "error":
|
|
508
|
+
result.finishReason = "error";
|
|
509
|
+
throw Object.assign(new Error(e.message), { code: e.code });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/providers/claude/parser.ts
|
|
516
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
517
|
+
var num = (v) => typeof v === "number" ? v : 0;
|
|
518
|
+
function toolOutput(content) {
|
|
519
|
+
if (typeof content === "string") return content;
|
|
520
|
+
if (!Array.isArray(content)) return void 0;
|
|
521
|
+
const text = content.flatMap((c) => isObj(c) && typeof c.text === "string" ? [c.text] : []);
|
|
522
|
+
return text.length ? text.join("\n") : void 0;
|
|
523
|
+
}
|
|
524
|
+
function parseClaudeMessage(raw) {
|
|
525
|
+
if (!isObj(raw)) return [];
|
|
526
|
+
const type = raw.type;
|
|
527
|
+
if (type === "system" && raw.subtype === "init" && typeof raw.session_id === "string") {
|
|
528
|
+
return [{ type: "session", sessionId: raw.session_id, model: typeof raw.model === "string" ? raw.model : void 0 }];
|
|
529
|
+
}
|
|
530
|
+
if (raw.parent_tool_use_id) return [];
|
|
531
|
+
if (type === "stream_event" && isObj(raw.event) && raw.event.type === "content_block_delta" && isObj(raw.event.delta)) {
|
|
532
|
+
const d = raw.event.delta;
|
|
533
|
+
if (d.type === "text_delta" && typeof d.text === "string") return [{ type: "text.delta", text: d.text }];
|
|
534
|
+
if (d.type === "thinking_delta" && typeof d.thinking === "string") return [{ type: "reasoning.delta", text: d.thinking }];
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
if (type === "assistant" && isObj(raw.message) && Array.isArray(raw.message.content)) {
|
|
538
|
+
return raw.message.content.flatMap(
|
|
539
|
+
(b) => isObj(b) && b.type === "tool_use" && typeof b.id === "string" ? [{ type: "tool.start", id: b.id, name: String(b.name), input: b.input }] : []
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (type === "user" && isObj(raw.message) && Array.isArray(raw.message.content)) {
|
|
543
|
+
return raw.message.content.flatMap((b) => {
|
|
544
|
+
if (!isObj(b) || b.type !== "tool_result" || typeof b.tool_use_id !== "string") return [];
|
|
545
|
+
const out = toolOutput(b.content);
|
|
546
|
+
return [
|
|
547
|
+
b.is_error === true ? { type: "tool.end", id: b.tool_use_id, error: out ?? "Tool failed." } : { type: "tool.end", id: b.tool_use_id, output: out }
|
|
548
|
+
];
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (type === "result") return parseResult(raw);
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
function parseResult(r) {
|
|
555
|
+
const text = typeof r.result === "string" ? r.result : "";
|
|
556
|
+
if (r.is_error === true || typeof r.subtype === "string" && r.subtype.startsWith("error")) {
|
|
557
|
+
const status = r.api_error_status;
|
|
558
|
+
return [{ type: "error", message: text || `Claude Code failed (${String(r.subtype)}).`, code: status ? String(status) : void 0 }];
|
|
559
|
+
}
|
|
560
|
+
const events = [];
|
|
561
|
+
if (isObj(r.usage)) {
|
|
562
|
+
const u = r.usage;
|
|
563
|
+
const cached = num(u.cache_read_input_tokens);
|
|
564
|
+
const input = num(u.input_tokens) + cached + num(u.cache_creation_input_tokens);
|
|
565
|
+
const usage = {
|
|
566
|
+
inputTokens: input,
|
|
567
|
+
outputTokens: num(u.output_tokens),
|
|
568
|
+
cachedInputTokens: cached,
|
|
569
|
+
totalTokens: input + num(u.output_tokens)
|
|
570
|
+
};
|
|
571
|
+
events.push({ type: "usage", usage });
|
|
572
|
+
}
|
|
573
|
+
const denied = Array.isArray(r.permission_denials) && r.permission_denials.length > 0;
|
|
574
|
+
const finishReason = denied ? "denied" : "stop";
|
|
575
|
+
events.push({ type: "done", text, finishReason });
|
|
576
|
+
return events;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/providers/claude/index.ts
|
|
580
|
+
var BIN = "claude";
|
|
581
|
+
var TESTED_RANGE = { min: "2.1.0" };
|
|
582
|
+
var PERMISSION_MODE = {
|
|
583
|
+
supervised: "manual",
|
|
584
|
+
"accept-edits": "acceptEdits",
|
|
585
|
+
auto: "auto",
|
|
586
|
+
full: "bypassPermissions"
|
|
587
|
+
};
|
|
588
|
+
var capabilities = {
|
|
589
|
+
access: ["supervised", "accept-edits", "auto", "full"],
|
|
590
|
+
effort: true,
|
|
591
|
+
contextWindow: false,
|
|
592
|
+
fast: false,
|
|
593
|
+
resume: true,
|
|
594
|
+
approvals: true
|
|
595
|
+
};
|
|
596
|
+
function buildArgs(input) {
|
|
597
|
+
const { selection } = input;
|
|
598
|
+
const args = [
|
|
599
|
+
"-p",
|
|
600
|
+
"--input-format",
|
|
601
|
+
"stream-json",
|
|
602
|
+
"--output-format",
|
|
603
|
+
"stream-json",
|
|
604
|
+
"--verbose",
|
|
605
|
+
"--include-partial-messages",
|
|
606
|
+
"--model",
|
|
607
|
+
selection.model,
|
|
608
|
+
"--permission-mode",
|
|
609
|
+
PERMISSION_MODE[selection.access]
|
|
610
|
+
];
|
|
611
|
+
if (selection.effort) args.push("--effort", selection.effort);
|
|
612
|
+
if (selection.access !== "full" && input.onApproval) args.push("--permission-prompt-tool", "stdio");
|
|
613
|
+
if (input.sessionId) args.push("--resume", input.sessionId);
|
|
614
|
+
return args;
|
|
615
|
+
}
|
|
616
|
+
async function* run(input) {
|
|
617
|
+
const bin = findBinary(BIN);
|
|
618
|
+
if (!bin) throw new FunnelError("Claude Code is not installed.", "not-installed");
|
|
619
|
+
if (input.selection.access === "supervised" && !input.onApproval) {
|
|
620
|
+
throw new FunnelError('Access "supervised" needs an onApproval handler.', "invalid-selection");
|
|
621
|
+
}
|
|
622
|
+
const proc = spawnStream(bin, buildArgs(input), { cwd: input.selection.cwd, env: input.env, signal: input.signal, closeStdin: false });
|
|
623
|
+
proc.write(JSON.stringify({ type: "user", message: { role: "user", content: input.prompt } }));
|
|
624
|
+
let finished = false;
|
|
625
|
+
try {
|
|
626
|
+
for await (const raw of proc.lines) {
|
|
627
|
+
const msg = raw;
|
|
628
|
+
if (msg.type === "control_request") {
|
|
629
|
+
const req = msg.request ?? {};
|
|
630
|
+
if (req.subtype !== "can_use_tool") {
|
|
631
|
+
proc.write(JSON.stringify({ type: "control_response", response: { subtype: "error", request_id: msg.request_id, error: "unsupported request" } }));
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const request = { id: String(req.tool_use_id ?? msg.request_id), tool: String(req.tool_name), input: req.input, description: req.description };
|
|
635
|
+
yield { type: "approval.request", request };
|
|
636
|
+
const decision = await input.onApproval(request);
|
|
637
|
+
const response = decision === "allow" ? { behavior: "allow", updatedInput: req.input } : { behavior: "deny", message: "Denied by the user." };
|
|
638
|
+
proc.write(JSON.stringify({ type: "control_response", response: { subtype: "success", request_id: msg.request_id, response } }));
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
for (const e of parseClaudeMessage(raw)) {
|
|
642
|
+
if (e.type === "done" || e.type === "error") finished = true;
|
|
643
|
+
yield e;
|
|
644
|
+
}
|
|
645
|
+
if (msg.type === "result") proc.closeStdin();
|
|
646
|
+
}
|
|
647
|
+
} finally {
|
|
648
|
+
if (!finished) proc.child.kill("SIGTERM");
|
|
649
|
+
}
|
|
650
|
+
if (finished) return;
|
|
651
|
+
await proc.exited;
|
|
652
|
+
if (input.signal?.aborted) yield { type: "done", text: "", finishReason: "cancelled" };
|
|
653
|
+
else yield { type: "error", message: proc.stderr().trim() || "Claude Code exited without a result.", code: "cli-failed" };
|
|
654
|
+
}
|
|
655
|
+
async function authStatus() {
|
|
656
|
+
const bin = findBinary(BIN);
|
|
657
|
+
if (!bin) return { loggedIn: false, detail: "Claude Code is not installed." };
|
|
658
|
+
const res = await exec(bin, ["auth", "status"], { timeoutMs: 15e3 });
|
|
659
|
+
try {
|
|
660
|
+
const j = JSON.parse(res.stdout);
|
|
661
|
+
return { loggedIn: j.loggedIn === true, method: j.authMethod, account: j.email, plan: j.subscriptionType };
|
|
662
|
+
} catch {
|
|
663
|
+
return { loggedIn: false, detail: (res.stderr || res.stdout).trim() || void 0 };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
var claudeProvider = {
|
|
667
|
+
id: "claude",
|
|
668
|
+
displayName: "Claude Code",
|
|
669
|
+
binary: BIN,
|
|
670
|
+
capabilities,
|
|
671
|
+
detect: () => detectInstallation(BIN, TESTED_RANGE),
|
|
672
|
+
authStatus,
|
|
673
|
+
login: (options) => spawnLogin(BIN, ["auth", "login"], authStatus, { signal: options?.signal }),
|
|
674
|
+
logout: () => runLogout(BIN, ["auth", "logout"]),
|
|
675
|
+
update: () => runUpdate(BIN, ["update"]),
|
|
676
|
+
async models() {
|
|
677
|
+
const [manifest, install] = await Promise.all([loadManifest(), claudeProvider.detect()]);
|
|
678
|
+
return mergeModels([], fromManifest(manifest, "claude", install.version, compareVersions));
|
|
679
|
+
},
|
|
680
|
+
run
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/providers/codex/access.ts
|
|
684
|
+
function planAccess(access) {
|
|
685
|
+
switch (access) {
|
|
686
|
+
case "supervised":
|
|
687
|
+
return { approvalPolicy: "untrusted", sandbox: "workspace-write", approvalsReviewer: "user", autoAllowFileChanges: false };
|
|
688
|
+
case "accept-edits":
|
|
689
|
+
return { approvalPolicy: "untrusted", sandbox: "workspace-write", approvalsReviewer: "user", autoAllowFileChanges: true };
|
|
690
|
+
case "auto":
|
|
691
|
+
return { approvalPolicy: "on-request", sandbox: "workspace-write", approvalsReviewer: "auto_review", autoAllowFileChanges: false };
|
|
692
|
+
case "full":
|
|
693
|
+
return { approvalPolicy: "never", sandbox: "danger-full-access", approvalsReviewer: "user", autoAllowFileChanges: true };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/providers/codex/approvals.ts
|
|
698
|
+
function toApprovalRequest(req, items) {
|
|
699
|
+
const p = req.params;
|
|
700
|
+
switch (req.method) {
|
|
701
|
+
case "item/commandExecution/requestApproval":
|
|
702
|
+
return {
|
|
703
|
+
id: String(p.approvalId ?? p.itemId),
|
|
704
|
+
tool: "command",
|
|
705
|
+
input: { command: p.command, cwd: p.cwd, reason: p.reason },
|
|
706
|
+
description: p.reason ?? p.command ?? void 0
|
|
707
|
+
};
|
|
708
|
+
case "item/fileChange/requestApproval":
|
|
709
|
+
return {
|
|
710
|
+
id: String(p.itemId),
|
|
711
|
+
tool: "file_change",
|
|
712
|
+
input: { changes: items.get(p.itemId), reason: p.reason, grantRoot: p.grantRoot },
|
|
713
|
+
description: p.reason ?? void 0
|
|
714
|
+
};
|
|
715
|
+
case "mcpServer/elicitation/request":
|
|
716
|
+
return { id: String(req.id), tool: "mcp", input: p, description: p.message ?? void 0 };
|
|
717
|
+
default:
|
|
718
|
+
return void 0;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function toReply(req, decision) {
|
|
722
|
+
const allow = decision === "allow";
|
|
723
|
+
switch (req.method) {
|
|
724
|
+
case "item/commandExecution/requestApproval":
|
|
725
|
+
case "item/fileChange/requestApproval":
|
|
726
|
+
return { result: { decision: allow ? "accept" : "decline" } };
|
|
727
|
+
case "mcpServer/elicitation/request":
|
|
728
|
+
return { result: { action: allow ? "accept" : "decline", content: null, _meta: null } };
|
|
729
|
+
default:
|
|
730
|
+
return { error: { code: -32601, message: `Unsupported request ${req.method}` } };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/providers/codex/auth.ts
|
|
735
|
+
function parseLoginStatus(text) {
|
|
736
|
+
const line = text.trim().split("\n")[0] ?? "";
|
|
737
|
+
if (/not logged in/i.test(line)) return { loggedIn: false, detail: line };
|
|
738
|
+
if (/logged in using chatgpt/i.test(line)) return { loggedIn: true, method: "ChatGPT", detail: line };
|
|
739
|
+
if (/logged in using an api key/i.test(line)) return { loggedIn: true, method: "API key", detail: "Logged in using an API key" };
|
|
740
|
+
const other = line.match(/logged in using (.+)/i);
|
|
741
|
+
if (other) return { loggedIn: true, method: other[1]?.trim(), detail: line };
|
|
742
|
+
return { loggedIn: false, detail: line || void 0 };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/providers/codex/events.ts
|
|
746
|
+
var Translator = class {
|
|
747
|
+
constructor(threadId) {
|
|
748
|
+
this.threadId = threadId;
|
|
749
|
+
}
|
|
750
|
+
threadId;
|
|
751
|
+
/** File changes by item id, kept so approval requests can show them. */
|
|
752
|
+
items = /* @__PURE__ */ new Map();
|
|
753
|
+
messages = [];
|
|
754
|
+
usage = { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, totalTokens: 0 };
|
|
755
|
+
handle(method, p = {}) {
|
|
756
|
+
if (p.threadId && p.threadId !== this.threadId) return { events: [] };
|
|
757
|
+
switch (method) {
|
|
758
|
+
case "item/agentMessage/delta":
|
|
759
|
+
return { events: [{ type: "text.delta", text: p.delta }] };
|
|
760
|
+
case "item/reasoning/summaryTextDelta":
|
|
761
|
+
case "item/reasoning/textDelta":
|
|
762
|
+
return { events: [{ type: "reasoning.delta", text: p.delta }] };
|
|
763
|
+
case "item/started":
|
|
764
|
+
return { events: this.itemStarted(p.item) };
|
|
765
|
+
case "item/completed":
|
|
766
|
+
return { events: this.itemCompleted(p.item) };
|
|
767
|
+
case "thread/tokenUsage/updated": {
|
|
768
|
+
const l = p.tokenUsage?.last;
|
|
769
|
+
if (!l) return { events: [] };
|
|
770
|
+
const u = this.usage;
|
|
771
|
+
u.inputTokens += l.inputTokens;
|
|
772
|
+
u.outputTokens += l.outputTokens;
|
|
773
|
+
u.cachedInputTokens = (u.cachedInputTokens ?? 0) + l.cachedInputTokens;
|
|
774
|
+
u.reasoningTokens = (u.reasoningTokens ?? 0) + l.reasoningOutputTokens;
|
|
775
|
+
u.totalTokens += l.totalTokens;
|
|
776
|
+
return { events: [{ type: "usage", usage: { ...u } }] };
|
|
777
|
+
}
|
|
778
|
+
case "error":
|
|
779
|
+
if (p.willRetry) return { events: [] };
|
|
780
|
+
return { events: [{ type: "error", message: p.error?.message ?? "Codex reported an error." }], finished: true };
|
|
781
|
+
case "turn/completed":
|
|
782
|
+
return this.turnCompleted(p.turn);
|
|
783
|
+
default:
|
|
784
|
+
return { events: [] };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
itemStarted(item) {
|
|
788
|
+
switch (item?.type) {
|
|
789
|
+
case "commandExecution":
|
|
790
|
+
return [{ type: "tool.start", id: item.id, name: "command", input: { command: item.command, cwd: item.cwd } }];
|
|
791
|
+
case "fileChange":
|
|
792
|
+
this.items.set(item.id, item.changes);
|
|
793
|
+
return [{ type: "tool.start", id: item.id, name: "file_change", input: { changes: item.changes } }];
|
|
794
|
+
case "mcpToolCall":
|
|
795
|
+
return [{ type: "tool.start", id: item.id, name: `mcp:${item.server}/${item.tool}`, input: item.arguments }];
|
|
796
|
+
case "dynamicToolCall":
|
|
797
|
+
return [{ type: "tool.start", id: item.id, name: item.tool, input: item.arguments }];
|
|
798
|
+
default:
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
itemCompleted(item) {
|
|
803
|
+
switch (item?.type) {
|
|
804
|
+
case "agentMessage":
|
|
805
|
+
if (item.text) this.messages.push(item.text);
|
|
806
|
+
return [];
|
|
807
|
+
case "commandExecution": {
|
|
808
|
+
const bad = item.status === "failed" || item.status === "declined" || (item.exitCode ?? 0) !== 0;
|
|
809
|
+
const code = item.exitCode != null ? ` (exit ${item.exitCode})` : "";
|
|
810
|
+
return [{ type: "tool.end", id: item.id, output: item.aggregatedOutput ?? void 0, error: bad ? `Command ${item.status}${code}` : void 0 }];
|
|
811
|
+
}
|
|
812
|
+
case "fileChange": {
|
|
813
|
+
const bad = item.status === "failed" || item.status === "declined";
|
|
814
|
+
return [{ type: "tool.end", id: item.id, error: bad ? `File change ${item.status}` : void 0 }];
|
|
815
|
+
}
|
|
816
|
+
case "mcpToolCall": {
|
|
817
|
+
const error = item.error?.message ?? (item.status === "failed" ? "MCP tool call failed" : void 0);
|
|
818
|
+
const out = item.result ? JSON.stringify(item.result.structuredContent ?? item.result.content) : void 0;
|
|
819
|
+
return [{ type: "tool.end", id: item.id, output: out, error }];
|
|
820
|
+
}
|
|
821
|
+
case "dynamicToolCall":
|
|
822
|
+
return [{ type: "tool.end", id: item.id, error: item.success === false ? "Tool call failed" : void 0 }];
|
|
823
|
+
default:
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
turnCompleted(turn) {
|
|
828
|
+
if (turn?.status === "failed") {
|
|
829
|
+
return { events: [{ type: "error", message: turn.error?.message ?? "Turn failed." }], finished: true };
|
|
830
|
+
}
|
|
831
|
+
const finishReason = turn?.status === "interrupted" ? "cancelled" : "stop";
|
|
832
|
+
return { events: [{ type: "done", text: this.messages.join("\n\n"), finishReason }], finished: true };
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// src/providers/codex/models.ts
|
|
837
|
+
var LABELS = { xhigh: "Extra high" };
|
|
838
|
+
var label = (id) => LABELS[id] ?? id.charAt(0).toUpperCase() + id.slice(1);
|
|
839
|
+
function parseCatalog(json) {
|
|
840
|
+
const data = JSON.parse(json);
|
|
841
|
+
return (data.models ?? []).filter((m) => m.visibility === "list").map((m) => ({
|
|
842
|
+
id: m.slug,
|
|
843
|
+
name: m.display_name,
|
|
844
|
+
provider: "codex",
|
|
845
|
+
efforts: (m.supported_reasoning_levels ?? []).map((l) => ({ id: l.effort, label: label(l.effort) })),
|
|
846
|
+
defaultEffort: m.default_reasoning_level,
|
|
847
|
+
contextWindows: [],
|
|
848
|
+
fast: !!m.additional_speed_tiers?.includes("fast") || !!m.service_tiers?.some((t) => t.id === "priority"),
|
|
849
|
+
source: "cli"
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/providers/codex/rpc.ts
|
|
854
|
+
var RpcClient = class {
|
|
855
|
+
constructor(proc, handlers) {
|
|
856
|
+
this.proc = proc;
|
|
857
|
+
void (async () => {
|
|
858
|
+
try {
|
|
859
|
+
for await (const raw of proc.lines) {
|
|
860
|
+
const m = raw;
|
|
861
|
+
if (m.method && m.id !== void 0) handlers.request(m.id, m.method, m.params);
|
|
862
|
+
else if (m.method) handlers.notification(m.method, m.params);
|
|
863
|
+
else if (typeof m.id === "number") {
|
|
864
|
+
const p = this.pending.get(m.id);
|
|
865
|
+
this.pending.delete(m.id);
|
|
866
|
+
if (m.error) p?.reject(new Error(m.error.message));
|
|
867
|
+
else p?.resolve(m.result);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
for (const p of this.pending.values()) p.reject(new Error("Codex app-server closed."));
|
|
873
|
+
this.pending.clear();
|
|
874
|
+
handlers.closed();
|
|
875
|
+
})();
|
|
876
|
+
}
|
|
877
|
+
proc;
|
|
878
|
+
nextId = 1;
|
|
879
|
+
pending = /* @__PURE__ */ new Map();
|
|
880
|
+
request(method, params) {
|
|
881
|
+
const id = this.nextId++;
|
|
882
|
+
return new Promise((resolve, reject) => {
|
|
883
|
+
this.pending.set(id, { resolve, reject });
|
|
884
|
+
this.proc.write(JSON.stringify({ method, id, params }));
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
notify(method) {
|
|
888
|
+
this.proc.write(JSON.stringify({ method }));
|
|
889
|
+
}
|
|
890
|
+
respond(id, reply) {
|
|
891
|
+
this.proc.write(JSON.stringify({ id, ...reply }));
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// src/providers/codex/index.ts
|
|
896
|
+
var BINARY = "codex";
|
|
897
|
+
var TESTED_RANGE2 = { min: "0.150.0" };
|
|
898
|
+
async function authStatus2() {
|
|
899
|
+
const path = findBinary(BINARY);
|
|
900
|
+
if (!path) return { loggedIn: false, detail: "codex is not installed." };
|
|
901
|
+
const res = await exec(path, ["login", "status"], { timeoutMs: 15e3 });
|
|
902
|
+
return parseLoginStatus(res.stdout + res.stderr);
|
|
903
|
+
}
|
|
904
|
+
async function models() {
|
|
905
|
+
const path = findBinary(BINARY);
|
|
906
|
+
if (path) {
|
|
907
|
+
try {
|
|
908
|
+
const res = await exec(path, ["debug", "models"], { timeoutMs: 2e4 });
|
|
909
|
+
const live = res.code === 0 ? parseCatalog(res.stdout) : [];
|
|
910
|
+
if (live.length) return live;
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return fromManifest(await loadBundledManifest(), "codex");
|
|
915
|
+
}
|
|
916
|
+
async function* run2(input) {
|
|
917
|
+
const path = findBinary(BINARY);
|
|
918
|
+
if (!path) {
|
|
919
|
+
yield { type: "error", message: "codex is not installed.", code: "not-installed" };
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const sel = input.selection;
|
|
923
|
+
const plan = planAccess(sel.access);
|
|
924
|
+
const out = new Channel();
|
|
925
|
+
const proc = spawnStream(path, ["app-server"], { cwd: sel.cwd, env: input.env });
|
|
926
|
+
let translator;
|
|
927
|
+
let threadId;
|
|
928
|
+
let turnId;
|
|
929
|
+
let finished = false;
|
|
930
|
+
const finish = (events) => {
|
|
931
|
+
if (finished) return;
|
|
932
|
+
finished = true;
|
|
933
|
+
for (const e of events) out.push(e);
|
|
934
|
+
out.end();
|
|
935
|
+
proc.child.kill("SIGTERM");
|
|
936
|
+
};
|
|
937
|
+
const decide = async (req) => {
|
|
938
|
+
if (req.method === "item/fileChange/requestApproval" && plan.autoAllowFileChanges) return "allow";
|
|
939
|
+
if (sel.access === "full") return "allow";
|
|
940
|
+
const request = toApprovalRequest(req, translator?.items ?? /* @__PURE__ */ new Map());
|
|
941
|
+
if (!request) return "deny";
|
|
942
|
+
out.push({ type: "approval.request", request });
|
|
943
|
+
if (!input.onApproval) return "deny";
|
|
944
|
+
try {
|
|
945
|
+
return await input.onApproval(request);
|
|
946
|
+
} catch {
|
|
947
|
+
return "deny";
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
const rpc = new RpcClient(proc, {
|
|
951
|
+
notification(method, params) {
|
|
952
|
+
if (!translator || finished) return;
|
|
953
|
+
if (method === "turn/started" && params?.threadId === threadId) turnId = params.turn?.id;
|
|
954
|
+
const t = translator.handle(method, params);
|
|
955
|
+
if (t.finished) finish(t.events);
|
|
956
|
+
else for (const e of t.events) out.push(e);
|
|
957
|
+
},
|
|
958
|
+
request(id, method, params) {
|
|
959
|
+
const req = { id, method, params: params ?? {} };
|
|
960
|
+
if (!toApprovalRequest(req, translator?.items ?? /* @__PURE__ */ new Map())) {
|
|
961
|
+
rpc.respond(id, toReply(req, "deny"));
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
void decide(req).then((d) => rpc.respond(id, toReply(req, d)));
|
|
965
|
+
},
|
|
966
|
+
closed() {
|
|
967
|
+
const err = proc.stderr().trim().split("\n").slice(-3).join(" ");
|
|
968
|
+
finish([{ type: "error", message: `Codex app-server exited early.${err ? ` ${err}` : ""}` }]);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
const onAbort = () => {
|
|
972
|
+
if (finished) return;
|
|
973
|
+
if (threadId && turnId) {
|
|
974
|
+
rpc.request("turn/interrupt", { threadId, turnId }).catch(() => {
|
|
975
|
+
});
|
|
976
|
+
setTimeout(() => finish([{ type: "done", text: "", finishReason: "cancelled" }]), 5e3).unref();
|
|
977
|
+
} else {
|
|
978
|
+
finish([{ type: "done", text: "", finishReason: "cancelled" }]);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
if (input.signal?.aborted) onAbort();
|
|
982
|
+
else input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
983
|
+
void (async () => {
|
|
984
|
+
try {
|
|
985
|
+
await rpc.request("initialize", {
|
|
986
|
+
clientInfo: { name: "cli-funnel", title: null, version: "0.1.0" },
|
|
987
|
+
capabilities: { experimentalApi: false, requestAttestation: false }
|
|
988
|
+
});
|
|
989
|
+
rpc.notify("initialized");
|
|
990
|
+
const settings = {
|
|
991
|
+
model: sel.model,
|
|
992
|
+
cwd: sel.cwd,
|
|
993
|
+
approvalPolicy: plan.approvalPolicy,
|
|
994
|
+
approvalsReviewer: plan.approvalsReviewer,
|
|
995
|
+
sandbox: plan.sandbox,
|
|
996
|
+
...sel.fast ? { serviceTier: "priority" } : {}
|
|
997
|
+
};
|
|
998
|
+
const started = input.sessionId ? await rpc.request("thread/resume", { threadId: input.sessionId, ...settings }) : await rpc.request("thread/start", settings);
|
|
999
|
+
threadId = started.thread.id;
|
|
1000
|
+
translator = new Translator(threadId);
|
|
1001
|
+
out.push({ type: "session", sessionId: threadId, model: started.model });
|
|
1002
|
+
const res = await rpc.request("turn/start", {
|
|
1003
|
+
threadId,
|
|
1004
|
+
input: [{ type: "text", text: input.prompt, text_elements: [] }],
|
|
1005
|
+
...sel.effort ? { effort: sel.effort } : {}
|
|
1006
|
+
});
|
|
1007
|
+
turnId ??= res?.turn?.id;
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
finish([{ type: "error", message: e instanceof Error ? e.message : String(e), code: "cli-failed" }]);
|
|
1010
|
+
}
|
|
1011
|
+
})();
|
|
1012
|
+
try {
|
|
1013
|
+
yield* out;
|
|
1014
|
+
} finally {
|
|
1015
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
1016
|
+
proc.child.kill("SIGTERM");
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
var codexProvider = {
|
|
1020
|
+
id: "codex",
|
|
1021
|
+
displayName: "Codex",
|
|
1022
|
+
binary: BINARY,
|
|
1023
|
+
capabilities: {
|
|
1024
|
+
access: ["supervised", "accept-edits", "auto", "full"],
|
|
1025
|
+
effort: true,
|
|
1026
|
+
contextWindow: false,
|
|
1027
|
+
fast: true,
|
|
1028
|
+
resume: true,
|
|
1029
|
+
approvals: true
|
|
1030
|
+
},
|
|
1031
|
+
detect: () => detectInstallation(BINARY, TESTED_RANGE2),
|
|
1032
|
+
authStatus: authStatus2,
|
|
1033
|
+
login: (options) => spawnLogin(BINARY, ["login", "--device-auth"], authStatus2, { signal: options?.signal }),
|
|
1034
|
+
logout: () => runLogout(BINARY, ["logout"]),
|
|
1035
|
+
update: () => runUpdate(BINARY, ["update"]),
|
|
1036
|
+
models,
|
|
1037
|
+
run: run2
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// src/providers/agent/parser.ts
|
|
1041
|
+
var EFFORT_ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1042
|
+
var EFFORT_LABEL = {
|
|
1043
|
+
none: "None",
|
|
1044
|
+
minimal: "Minimal",
|
|
1045
|
+
low: "Low",
|
|
1046
|
+
medium: "Medium",
|
|
1047
|
+
high: "High",
|
|
1048
|
+
xhigh: "Extra High",
|
|
1049
|
+
max: "Max"
|
|
1050
|
+
};
|
|
1051
|
+
var EFFORT_WORDS = /\b(?:Minimal|None|Low|Medium|High|Extra High|Max)\b/g;
|
|
1052
|
+
function parseModelList(text) {
|
|
1053
|
+
const out = [];
|
|
1054
|
+
for (const raw of text.replace(/[-]/g, "").split("\n")) {
|
|
1055
|
+
const m = raw.match(/^([a-z0-9][\w.-]*) - (.+)$/i);
|
|
1056
|
+
if (m?.[1] && m[2] && m[1] !== "auto") out.push({ id: m[1], name: m[2].trim() });
|
|
1057
|
+
}
|
|
1058
|
+
return out;
|
|
1059
|
+
}
|
|
1060
|
+
function splitId(id) {
|
|
1061
|
+
let tokens = id.split("-");
|
|
1062
|
+
let fast = false;
|
|
1063
|
+
let thinking = false;
|
|
1064
|
+
let effort;
|
|
1065
|
+
if (tokens.length > 1 && tokens[tokens.length - 1] === "fast") {
|
|
1066
|
+
fast = true;
|
|
1067
|
+
tokens = tokens.slice(0, -1);
|
|
1068
|
+
}
|
|
1069
|
+
const dropThinking = () => {
|
|
1070
|
+
if (tokens[tokens.length - 1] === "thinking") {
|
|
1071
|
+
thinking = true;
|
|
1072
|
+
tokens = tokens.slice(0, -1);
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
dropThinking();
|
|
1076
|
+
const tail = tokens.slice(-2).join("-");
|
|
1077
|
+
if (tail === "extra-high") {
|
|
1078
|
+
effort = "xhigh";
|
|
1079
|
+
tokens = tokens.slice(0, -2);
|
|
1080
|
+
} else if (EFFORT_ORDER.includes(tokens[tokens.length - 1] ?? "")) {
|
|
1081
|
+
effort = tokens[tokens.length - 1];
|
|
1082
|
+
tokens = tokens.slice(0, -1);
|
|
1083
|
+
}
|
|
1084
|
+
dropThinking();
|
|
1085
|
+
const stem = tokens.join("-");
|
|
1086
|
+
return { base: thinking ? `${stem}-thinking` : stem, thinking, effort, fast, bare: effort === void 0 };
|
|
1087
|
+
}
|
|
1088
|
+
function groupModels(listed) {
|
|
1089
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1090
|
+
for (const { id, name } of listed) {
|
|
1091
|
+
const p = splitId(id);
|
|
1092
|
+
const g = groups.get(p.base) ?? { name: "", efforts: /* @__PURE__ */ new Set(), fast: false, bare: false };
|
|
1093
|
+
const clean = name.replace(EFFORT_WORDS, "").replace(/\bFast\b/g, "").replace(/\s+/g, " ").trim();
|
|
1094
|
+
if (!g.name || clean.length < g.name.length) g.name = clean;
|
|
1095
|
+
if (p.effort) g.efforts.add(p.effort);
|
|
1096
|
+
if (p.fast) g.fast = true;
|
|
1097
|
+
if (p.bare) g.bare = true;
|
|
1098
|
+
groups.set(p.base, g);
|
|
1099
|
+
}
|
|
1100
|
+
const models2 = [];
|
|
1101
|
+
for (const [id, g] of groups) {
|
|
1102
|
+
if (g.bare && g.efforts.size && id.startsWith("gpt-")) g.efforts.add("medium");
|
|
1103
|
+
const efforts = EFFORT_ORDER.filter((e) => g.efforts.has(e)).map((e) => ({ id: e, label: EFFORT_LABEL[e] ?? e }));
|
|
1104
|
+
const defaultEffort = ["medium", "high"].find((e) => g.efforts.has(e)) ?? efforts[0]?.id;
|
|
1105
|
+
models2.push({
|
|
1106
|
+
id,
|
|
1107
|
+
name: g.name,
|
|
1108
|
+
provider: "agent",
|
|
1109
|
+
efforts,
|
|
1110
|
+
...defaultEffort ? { defaultEffort } : {},
|
|
1111
|
+
contextWindows: [],
|
|
1112
|
+
fast: g.fast,
|
|
1113
|
+
source: "cli"
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
return models2;
|
|
1117
|
+
}
|
|
1118
|
+
function toCliModel(selection, known2 = /* @__PURE__ */ new Set()) {
|
|
1119
|
+
const thinking = selection.model.endsWith("-thinking");
|
|
1120
|
+
const stem = thinking ? selection.model.slice(0, -"-thinking".length) : selection.model;
|
|
1121
|
+
const efforts = selection.effort ? [selection.effort] : ["", "medium", "high", "low", "xhigh", "max", "minimal", "none"];
|
|
1122
|
+
const candidates = [];
|
|
1123
|
+
for (const e of efforts) {
|
|
1124
|
+
const tokens = e === "xhigh" ? ["xhigh", "extra-high"] : [e];
|
|
1125
|
+
for (const t of tokens) {
|
|
1126
|
+
const parts = thinking ? [t ? `${stem}-thinking-${t}` : `${stem}-thinking`, ...t ? [`${stem}-${t}-thinking`] : []] : [t ? `${stem}-${t}` : stem];
|
|
1127
|
+
for (const p of parts) candidates.push(selection.fast ? `${p}-fast` : p);
|
|
1128
|
+
}
|
|
1129
|
+
if (e === "medium" && stem.startsWith("gpt-")) candidates.push(selection.fast ? `${selection.model}-fast` : selection.model);
|
|
1130
|
+
}
|
|
1131
|
+
if (!known2.size) return candidates[0];
|
|
1132
|
+
const hit = candidates.find((c) => known2.has(c));
|
|
1133
|
+
if (!hit) {
|
|
1134
|
+
throw new FunnelError(
|
|
1135
|
+
`Cursor Agent has no variant of ${selection.model} with effort ${selection.effort ?? "default"}${selection.fast ? " and fast tier" : ""}.`,
|
|
1136
|
+
"invalid-selection"
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return hit;
|
|
1140
|
+
}
|
|
1141
|
+
function parseStatus(json, aboutJson) {
|
|
1142
|
+
let s;
|
|
1143
|
+
try {
|
|
1144
|
+
s = JSON.parse(json);
|
|
1145
|
+
} catch {
|
|
1146
|
+
return { loggedIn: /logged in/i.test(json) && !/not logged in/i.test(json), account: json.match(/Logged in as (\S+)/i)?.[1] };
|
|
1147
|
+
}
|
|
1148
|
+
const status = { loggedIn: s.isAuthenticated === true, method: "Cursor", account: s.userInfo?.email };
|
|
1149
|
+
if (aboutJson) {
|
|
1150
|
+
try {
|
|
1151
|
+
const plan = JSON.parse(aboutJson).subscriptionTier;
|
|
1152
|
+
if (typeof plan === "string") status.plan = plan;
|
|
1153
|
+
} catch {
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return status;
|
|
1157
|
+
}
|
|
1158
|
+
function toolName(key) {
|
|
1159
|
+
return key.replace(/ToolCall$/, "");
|
|
1160
|
+
}
|
|
1161
|
+
function toolInput(name, args) {
|
|
1162
|
+
if (!args) return void 0;
|
|
1163
|
+
if (name === "shell") return { command: args.command, workingDirectory: args.workingDirectory || void 0 };
|
|
1164
|
+
return args;
|
|
1165
|
+
}
|
|
1166
|
+
function failure(result) {
|
|
1167
|
+
if (!result || "success" in result) return void 0;
|
|
1168
|
+
const e = result.error ?? result.failure ?? result.rejected ?? result.permissionDenied;
|
|
1169
|
+
const msg = e && (e.errorMessage ?? e.message ?? e.reason);
|
|
1170
|
+
return typeof msg === "string" ? msg : JSON.stringify(result).slice(0, 300);
|
|
1171
|
+
}
|
|
1172
|
+
function output(result) {
|
|
1173
|
+
const s = result?.success;
|
|
1174
|
+
if (!s) return void 0;
|
|
1175
|
+
for (const k of ["stdout", "content", "message"]) if (typeof s[k] === "string") return s[k];
|
|
1176
|
+
return void 0;
|
|
1177
|
+
}
|
|
1178
|
+
function toUsage(u) {
|
|
1179
|
+
if (!u) return void 0;
|
|
1180
|
+
const cached = u.cacheReadTokens ?? 0;
|
|
1181
|
+
const inputTokens = (u.inputTokens ?? 0) + cached + (u.cacheWriteTokens ?? 0);
|
|
1182
|
+
const outputTokens = u.outputTokens ?? 0;
|
|
1183
|
+
return { inputTokens, outputTokens, cachedInputTokens: cached, totalTokens: inputTokens + outputTokens };
|
|
1184
|
+
}
|
|
1185
|
+
var StreamMapper = class {
|
|
1186
|
+
sawPartial = false;
|
|
1187
|
+
text = "";
|
|
1188
|
+
denied = false;
|
|
1189
|
+
started = /* @__PURE__ */ new Map();
|
|
1190
|
+
map(line) {
|
|
1191
|
+
const m = line;
|
|
1192
|
+
if (!m || typeof m !== "object" || "__raw" in m) return [];
|
|
1193
|
+
switch (m.type) {
|
|
1194
|
+
case "system":
|
|
1195
|
+
return m.subtype === "init" && m.session_id ? [{ type: "session", sessionId: m.session_id }] : [];
|
|
1196
|
+
case "thinking":
|
|
1197
|
+
return m.subtype === "delta" && m.text ? [{ type: "reasoning.delta", text: m.text }] : [];
|
|
1198
|
+
case "assistant": {
|
|
1199
|
+
const partial = m.timestamp_ms !== void 0;
|
|
1200
|
+
if (partial) this.sawPartial = true;
|
|
1201
|
+
else if (this.sawPartial) return [];
|
|
1202
|
+
const text = (m.message?.content ?? []).map((c) => c.type === "text" ? c.text : "").join("");
|
|
1203
|
+
if (!text) return [];
|
|
1204
|
+
this.text += text;
|
|
1205
|
+
return [{ type: "text.delta", text }];
|
|
1206
|
+
}
|
|
1207
|
+
case "tool_call": {
|
|
1208
|
+
const key = Object.keys(m.tool_call ?? {})[0];
|
|
1209
|
+
if (!key) return [];
|
|
1210
|
+
const call = m.tool_call[key];
|
|
1211
|
+
const name = toolName(key);
|
|
1212
|
+
const id = String(m.call_id ?? call.toolCallId ?? name);
|
|
1213
|
+
if (m.subtype === "started") {
|
|
1214
|
+
this.started.set(id, name);
|
|
1215
|
+
return [{ type: "tool.start", id, name, input: toolInput(name, call.args) }];
|
|
1216
|
+
}
|
|
1217
|
+
if (m.subtype === "completed") {
|
|
1218
|
+
const error = failure(call.result);
|
|
1219
|
+
if (error && call.result && ("rejected" in call.result || "permissionDenied" in call.result)) this.denied = true;
|
|
1220
|
+
return [{ type: "tool.end", id, ...error ? { error } : { output: output(call.result) } }];
|
|
1221
|
+
}
|
|
1222
|
+
return [];
|
|
1223
|
+
}
|
|
1224
|
+
case "result": {
|
|
1225
|
+
const events = [];
|
|
1226
|
+
if (m.is_error || m.subtype !== "success") {
|
|
1227
|
+
return [{ type: "error", message: String(m.result ?? m.subtype ?? "Cursor Agent failed") }];
|
|
1228
|
+
}
|
|
1229
|
+
const usage = toUsage(m.usage);
|
|
1230
|
+
if (usage) events.push({ type: "usage", usage });
|
|
1231
|
+
events.push({ type: "done", text: typeof m.result === "string" ? m.result : this.text, finishReason: this.denied ? "denied" : "stop" });
|
|
1232
|
+
return events;
|
|
1233
|
+
}
|
|
1234
|
+
default:
|
|
1235
|
+
return [];
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
// src/providers/agent/index.ts
|
|
1241
|
+
var BINARY2 = "agent";
|
|
1242
|
+
var TESTED_RANGE3 = { min: "2026.09.01" };
|
|
1243
|
+
var known;
|
|
1244
|
+
async function listModels() {
|
|
1245
|
+
const path = findBinary(BINARY2);
|
|
1246
|
+
if (!path) return [];
|
|
1247
|
+
const res = await exec(path, ["--list-models"], { timeoutMs: 3e4 }).catch(() => void 0);
|
|
1248
|
+
return res ? parseModelList(res.stdout) : [];
|
|
1249
|
+
}
|
|
1250
|
+
async function knownIds() {
|
|
1251
|
+
if (!known?.size) known = new Set((await listModels()).map((m) => m.id));
|
|
1252
|
+
return known;
|
|
1253
|
+
}
|
|
1254
|
+
function accessFlags(access) {
|
|
1255
|
+
if (access === "full") return ["--force"];
|
|
1256
|
+
if (access === "auto") return ["--auto-review"];
|
|
1257
|
+
throw new FunnelError(`Cursor Agent cannot enforce access "${access}". Supported: auto, full.`, "invalid-selection");
|
|
1258
|
+
}
|
|
1259
|
+
function buildArgs2(input, model) {
|
|
1260
|
+
const { selection } = input;
|
|
1261
|
+
return [
|
|
1262
|
+
"-p",
|
|
1263
|
+
"--output-format",
|
|
1264
|
+
"stream-json",
|
|
1265
|
+
"--stream-partial-output",
|
|
1266
|
+
"--model",
|
|
1267
|
+
model,
|
|
1268
|
+
"--workspace",
|
|
1269
|
+
selection.cwd,
|
|
1270
|
+
"--trust",
|
|
1271
|
+
...accessFlags(selection.access),
|
|
1272
|
+
...input.sessionId ? ["--resume", input.sessionId] : [],
|
|
1273
|
+
"--",
|
|
1274
|
+
input.prompt
|
|
1275
|
+
];
|
|
1276
|
+
}
|
|
1277
|
+
async function authStatus3() {
|
|
1278
|
+
const path = findBinary(BINARY2);
|
|
1279
|
+
if (!path) return { loggedIn: false, detail: "agent is not installed." };
|
|
1280
|
+
const status = await exec(path, ["status", "--format", "json"], { timeoutMs: 15e3 }).catch(() => void 0);
|
|
1281
|
+
if (!status) return { loggedIn: false };
|
|
1282
|
+
const parsed = parseStatus(status.stdout || status.stderr);
|
|
1283
|
+
if (!parsed.loggedIn) return { loggedIn: false };
|
|
1284
|
+
const about = await exec(path, ["about", "--format", "json"], { timeoutMs: 15e3 }).catch(() => void 0);
|
|
1285
|
+
return parseStatus(status.stdout, about?.stdout);
|
|
1286
|
+
}
|
|
1287
|
+
async function* run3(input) {
|
|
1288
|
+
const path = findBinary(BINARY2);
|
|
1289
|
+
if (!path) throw new FunnelError("agent is not installed.", "not-installed");
|
|
1290
|
+
const model = toCliModel(input.selection, await knownIds());
|
|
1291
|
+
const args = buildArgs2(input, model);
|
|
1292
|
+
const proc = spawnStream(path, args, { cwd: input.selection.cwd, env: input.env, signal: input.signal, closeStdin: true });
|
|
1293
|
+
const mapper = new StreamMapper();
|
|
1294
|
+
let finished = false;
|
|
1295
|
+
for await (const line of proc.lines) {
|
|
1296
|
+
for (const event of mapper.map(line)) {
|
|
1297
|
+
if (event.type === "done" || event.type === "error") finished = true;
|
|
1298
|
+
yield event;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const code = await proc.exited;
|
|
1302
|
+
if (input.signal?.aborted) {
|
|
1303
|
+
yield { type: "done", text: "", finishReason: "cancelled" };
|
|
1304
|
+
} else if (!finished) {
|
|
1305
|
+
const detail = proc.stderr().trim().slice(-500);
|
|
1306
|
+
yield {
|
|
1307
|
+
type: "error",
|
|
1308
|
+
message: detail || `agent exited with code ${code} before finishing.`,
|
|
1309
|
+
code: /authentication required|not logged in/i.test(detail) ? "not-logged-in" : "cli-failed"
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
var agentProvider = {
|
|
1314
|
+
id: "agent",
|
|
1315
|
+
displayName: "Cursor Agent",
|
|
1316
|
+
binary: BINARY2,
|
|
1317
|
+
capabilities: { access: ["auto", "full"], effort: true, contextWindow: false, fast: true, resume: true, approvals: false },
|
|
1318
|
+
detect: () => detectInstallation(BINARY2, TESTED_RANGE3),
|
|
1319
|
+
authStatus: authStatus3,
|
|
1320
|
+
login: (options) => spawnLogin(BINARY2, ["login"], authStatus3, {
|
|
1321
|
+
signal: options?.signal,
|
|
1322
|
+
env: process.env.CLI_FUNNEL_OPEN_BROWSER === "1" ? {} : { NO_OPEN_BROWSER: "1" }
|
|
1323
|
+
}),
|
|
1324
|
+
logout: () => runLogout(BINARY2, ["logout"]),
|
|
1325
|
+
update: () => runUpdate(BINARY2, ["update"]),
|
|
1326
|
+
async models() {
|
|
1327
|
+
const listed = await listModels();
|
|
1328
|
+
if (listed.length) {
|
|
1329
|
+
known = new Set(listed.map((m) => m.id));
|
|
1330
|
+
return groupModels(listed);
|
|
1331
|
+
}
|
|
1332
|
+
return fromManifest(await loadBundledManifest(), "agent", void 0, compareVersions);
|
|
1333
|
+
},
|
|
1334
|
+
run: run3
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
// src/providers/antigravity/index.ts
|
|
1338
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1339
|
+
import { homedir as homedir2 } from "os";
|
|
1340
|
+
import { join as join2 } from "path";
|
|
1341
|
+
|
|
1342
|
+
// src/providers/antigravity/parser.ts
|
|
1343
|
+
var EFFORTS = ["low", "medium", "high", "max"];
|
|
1344
|
+
var EFFORT_RE = /^(.+)-(low|medium|high|max)$/;
|
|
1345
|
+
function parseModels(text) {
|
|
1346
|
+
const rows = [];
|
|
1347
|
+
for (const line of text.split("\n")) {
|
|
1348
|
+
const [rawId = "", rawName = ""] = line.split(" ");
|
|
1349
|
+
const id = rawId.trim();
|
|
1350
|
+
if (!id || id.includes(" ") || !rawName.trim()) continue;
|
|
1351
|
+
const m = EFFORT_RE.exec(id);
|
|
1352
|
+
rows.push({ id, name: rawName.trim(), base: m?.[1] ?? id, effort: m?.[2] });
|
|
1353
|
+
}
|
|
1354
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1355
|
+
for (const r of rows) groups.set(r.base, [...groups.get(r.base) ?? [], r]);
|
|
1356
|
+
const out = [];
|
|
1357
|
+
for (const [base, members] of groups) {
|
|
1358
|
+
const first = members[0];
|
|
1359
|
+
if (!first) continue;
|
|
1360
|
+
const base0 = { provider: "antigravity", contextWindows: [], fast: false, source: "cli" };
|
|
1361
|
+
if (members.length < 2 || members.some((x) => !x.effort)) {
|
|
1362
|
+
for (const r of members) out.push({ ...base0, id: r.id, name: r.name, efforts: [] });
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const efforts = members.map((x) => x.effort).sort((a, b) => EFFORTS.indexOf(a) - EFFORTS.indexOf(b));
|
|
1366
|
+
out.push({
|
|
1367
|
+
...base0,
|
|
1368
|
+
id: base,
|
|
1369
|
+
name: first.name.replace(/\s*\([^)]*\)\s*$/, ""),
|
|
1370
|
+
efforts: efforts.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
|
|
1371
|
+
defaultEffort: efforts.includes("high") ? "high" : efforts[0]
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
return out;
|
|
1375
|
+
}
|
|
1376
|
+
function toUsage2(u) {
|
|
1377
|
+
if (!u) return void 0;
|
|
1378
|
+
const input = u.input_tokens ?? 0;
|
|
1379
|
+
const output2 = u.output_tokens ?? 0;
|
|
1380
|
+
return {
|
|
1381
|
+
inputTokens: input,
|
|
1382
|
+
outputTokens: output2,
|
|
1383
|
+
cachedInputTokens: u.cache_read_tokens || void 0,
|
|
1384
|
+
reasoningTokens: u.thinking_tokens || void 0,
|
|
1385
|
+
totalTokens: u.total_tokens ?? input + output2
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function createMapper() {
|
|
1389
|
+
const started = /* @__PURE__ */ new Set();
|
|
1390
|
+
let deniedSeen = 0;
|
|
1391
|
+
let finished = false;
|
|
1392
|
+
return {
|
|
1393
|
+
get finished() {
|
|
1394
|
+
return finished;
|
|
1395
|
+
},
|
|
1396
|
+
map(line) {
|
|
1397
|
+
const o = line;
|
|
1398
|
+
if (!o || typeof o !== "object") return [];
|
|
1399
|
+
if (o.event === "init") {
|
|
1400
|
+
const id = o.conversation_id;
|
|
1401
|
+
return id ? [{ type: "session", sessionId: id, model: o.init?.model }] : [];
|
|
1402
|
+
}
|
|
1403
|
+
if (o.event === "step_update") {
|
|
1404
|
+
const s = o.step_update;
|
|
1405
|
+
if (s.step_type === "agent_response") {
|
|
1406
|
+
return s.text_delta ? [{ type: "text.delta", text: s.text_delta }] : [];
|
|
1407
|
+
}
|
|
1408
|
+
if (s.step_type !== "tool") return [];
|
|
1409
|
+
const id = `${s.conversation_id}:${s.step_index}`;
|
|
1410
|
+
const name = s.tool_name ?? s.tool_info?.name ?? "tool";
|
|
1411
|
+
const events = [];
|
|
1412
|
+
if (!started.has(id)) {
|
|
1413
|
+
started.add(id);
|
|
1414
|
+
events.push({ type: "tool.start", id, name, input: s.tool_info?.parameters });
|
|
1415
|
+
}
|
|
1416
|
+
if (s.state === "DONE") events.push({ type: "tool.end", id });
|
|
1417
|
+
if (s.state === "ERROR") {
|
|
1418
|
+
const msg = String(s.tool_info?.error?.message ?? "tool failed").split("\n")[0] ?? "tool failed";
|
|
1419
|
+
const denied = /denied permission|permission check failed/i.test(msg);
|
|
1420
|
+
if (denied) deniedSeen++;
|
|
1421
|
+
events.push({ type: "tool.end", id, error: denied ? `denied: ${msg}` : msg });
|
|
1422
|
+
}
|
|
1423
|
+
return events;
|
|
1424
|
+
}
|
|
1425
|
+
if (o.event === "result") {
|
|
1426
|
+
finished = true;
|
|
1427
|
+
const r = o.result;
|
|
1428
|
+
if (r.status === "ERROR" || r.error) {
|
|
1429
|
+
return [{ type: "error", message: String(r.error ?? "Antigravity run failed."), code: "cli-failed" }];
|
|
1430
|
+
}
|
|
1431
|
+
const events = [];
|
|
1432
|
+
const denied = r.denied_actions ?? [];
|
|
1433
|
+
denied.slice(deniedSeen).forEach((d, i) => {
|
|
1434
|
+
const id = `denied:${i}`;
|
|
1435
|
+
const name = d.display_name ?? d.action;
|
|
1436
|
+
events.push({ type: "tool.start", id, name });
|
|
1437
|
+
events.push({ type: "tool.end", id, error: `denied: ${d.action} needs permission that headless mode cannot ask for` });
|
|
1438
|
+
});
|
|
1439
|
+
const usage = toUsage2(r.usage);
|
|
1440
|
+
if (usage) events.push({ type: "usage", usage });
|
|
1441
|
+
const text = r.response ?? "";
|
|
1442
|
+
events.push({ type: "done", text, finishReason: denied.length && !text.trim() ? "denied" : "stop" });
|
|
1443
|
+
return events;
|
|
1444
|
+
}
|
|
1445
|
+
return [];
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/providers/antigravity/index.ts
|
|
1451
|
+
var TOKEN_FILE = join2(homedir2(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
|
|
1452
|
+
var TESTED = { min: "1.2.0" };
|
|
1453
|
+
function toCliModel2(selection, models2 = []) {
|
|
1454
|
+
const info = models2.find((m) => m.id === selection.model);
|
|
1455
|
+
const effort = selection.effort ?? (info?.efforts.length ? info.defaultEffort : void 0);
|
|
1456
|
+
return { model: selection.model, effort };
|
|
1457
|
+
}
|
|
1458
|
+
function buildArgs3(input, models2 = []) {
|
|
1459
|
+
const { selection } = input;
|
|
1460
|
+
const { model, effort } = toCliModel2(selection, models2);
|
|
1461
|
+
const args = ["--print=" + input.prompt, "--output-format", "stream-json", "--model", model];
|
|
1462
|
+
if (effort) args.push("--effort", effort);
|
|
1463
|
+
if (selection.access === "accept-edits") args.push("--mode", "accept-edits");
|
|
1464
|
+
if (selection.access === "full") args.push("--dangerously-skip-permissions");
|
|
1465
|
+
if (input.sessionId) args.push("--conversation", input.sessionId);
|
|
1466
|
+
return args;
|
|
1467
|
+
}
|
|
1468
|
+
async function loadManifestModels() {
|
|
1469
|
+
return fromManifest(await loadBundledManifest(), "antigravity");
|
|
1470
|
+
}
|
|
1471
|
+
async function authStatus4() {
|
|
1472
|
+
const path = findBinary("agy");
|
|
1473
|
+
if (!path) return { loggedIn: false, detail: "agy is not installed." };
|
|
1474
|
+
const apiKey = !!process.env.GEMINI_API_KEY;
|
|
1475
|
+
if (!apiKey && !existsSync4(TOKEN_FILE)) return { loggedIn: false, detail: "Run agy in a terminal and sign in." };
|
|
1476
|
+
const res = await exec(path, ["models"], { timeoutMs: 2e4 }).catch(() => void 0);
|
|
1477
|
+
const ok = !!res && res.code === 0 && parseModels(res.stdout).length > 0;
|
|
1478
|
+
return ok ? { loggedIn: true, method: apiKey ? "Gemini API key" : "Google account" } : { loggedIn: false, detail: "agy could not list models. Sign in again." };
|
|
1479
|
+
}
|
|
1480
|
+
function login(options = {}) {
|
|
1481
|
+
const channel = new Channel();
|
|
1482
|
+
let cancelled = false;
|
|
1483
|
+
const cancel = () => {
|
|
1484
|
+
cancelled = true;
|
|
1485
|
+
channel.end();
|
|
1486
|
+
};
|
|
1487
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
1488
|
+
channel.push({ type: "needs-terminal", command: ["agy"] });
|
|
1489
|
+
void (async () => {
|
|
1490
|
+
const deadline = Date.now() + 10 * 6e4;
|
|
1491
|
+
while (!cancelled && Date.now() < deadline) {
|
|
1492
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1493
|
+
if (cancelled) return;
|
|
1494
|
+
const status = await authStatus4();
|
|
1495
|
+
if (status.loggedIn) {
|
|
1496
|
+
channel.push({ type: "done", status });
|
|
1497
|
+
return channel.end();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (!cancelled) channel.push({ type: "error", message: "Timed out waiting for sign-in." });
|
|
1501
|
+
channel.end();
|
|
1502
|
+
})();
|
|
1503
|
+
return { [Symbol.asyncIterator]: () => channel[Symbol.asyncIterator](), sendCode: () => {
|
|
1504
|
+
}, cancel };
|
|
1505
|
+
}
|
|
1506
|
+
async function* run4(input) {
|
|
1507
|
+
const path = findBinary("agy");
|
|
1508
|
+
if (!path) throw new FunnelError("agy is not installed.", "not-installed");
|
|
1509
|
+
const models2 = await loadManifestModels();
|
|
1510
|
+
const stream = spawnStream(path, buildArgs3(input, models2), {
|
|
1511
|
+
cwd: input.selection.cwd,
|
|
1512
|
+
env: input.env,
|
|
1513
|
+
signal: input.signal,
|
|
1514
|
+
input: ""
|
|
1515
|
+
});
|
|
1516
|
+
const mapper = createMapper();
|
|
1517
|
+
for await (const line of stream.lines) {
|
|
1518
|
+
for (const e of mapper.map(line)) yield e;
|
|
1519
|
+
}
|
|
1520
|
+
const code = await stream.exited;
|
|
1521
|
+
if (mapper.finished) return;
|
|
1522
|
+
if (input.signal?.aborted) return void (yield { type: "done", text: "", finishReason: "cancelled" });
|
|
1523
|
+
const detail = stream.stderr().trim().split("\n").slice(-3).join(" ");
|
|
1524
|
+
yield { type: "error", message: `agy exited (${code}) without a result. ${detail}`.trim(), code: "cli-failed" };
|
|
1525
|
+
}
|
|
1526
|
+
var antigravityProvider = {
|
|
1527
|
+
id: "antigravity",
|
|
1528
|
+
displayName: "Antigravity",
|
|
1529
|
+
binary: "agy",
|
|
1530
|
+
capabilities: { access: ["accept-edits", "full"], effort: true, contextWindow: false, fast: false, resume: true, approvals: false },
|
|
1531
|
+
detect: () => detectInstallation("agy", TESTED),
|
|
1532
|
+
authStatus: authStatus4,
|
|
1533
|
+
login,
|
|
1534
|
+
async logout() {
|
|
1535
|
+
throw new FunnelError(
|
|
1536
|
+
"agy has no sign-out command. Run agy, open its account menu to sign out, or delete the token file in ~/.gemini/antigravity-cli/.",
|
|
1537
|
+
"unsupported"
|
|
1538
|
+
);
|
|
1539
|
+
},
|
|
1540
|
+
update: () => runUpdate("agy", ["update"]),
|
|
1541
|
+
async models() {
|
|
1542
|
+
const fallback = await loadManifestModels();
|
|
1543
|
+
const path = findBinary("agy");
|
|
1544
|
+
if (!path) return fallback;
|
|
1545
|
+
const res = await exec(path, ["models"], { timeoutMs: 2e4 }).catch(() => void 0);
|
|
1546
|
+
const live = res && res.code === 0 ? parseModels(res.stdout) : [];
|
|
1547
|
+
return live.length ? mergeModels(live, fallback) : fallback;
|
|
1548
|
+
},
|
|
1549
|
+
run: run4
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
// src/providers/index.ts
|
|
1553
|
+
var PROVIDERS = {
|
|
1554
|
+
claude: claudeProvider,
|
|
1555
|
+
codex: codexProvider,
|
|
1556
|
+
agent: agentProvider,
|
|
1557
|
+
antigravity: antigravityProvider,
|
|
1558
|
+
...createApiProviders()
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
// src/funnel.ts
|
|
1562
|
+
function createFunnel(options = {}) {
|
|
1563
|
+
const registry = { ...PROVIDERS, ...options.apiKeys ? createApiProviders(options.apiKeys) : {}, ...options.providers };
|
|
1564
|
+
const provider = (id) => {
|
|
1565
|
+
const p = registry[id];
|
|
1566
|
+
if (!p) throw new FunnelError(`Unknown provider "${id}".`, "invalid-selection");
|
|
1567
|
+
return p;
|
|
1568
|
+
};
|
|
1569
|
+
async function prepare(input) {
|
|
1570
|
+
const p = provider(input.selection.provider);
|
|
1571
|
+
const install = await p.detect();
|
|
1572
|
+
if (!install.installed) throw new FunnelError(`${p.displayName} CLI is not installed.`, "not-installed");
|
|
1573
|
+
const models2 = options.allowUnlistedModels ? void 0 : await p.models();
|
|
1574
|
+
validateSelection(input.selection, p, models2);
|
|
1575
|
+
if (input.selection.access === "supervised" && !input.onApproval) {
|
|
1576
|
+
throw new FunnelError('access "supervised" needs an onApproval callback.', "invalid-selection");
|
|
1577
|
+
}
|
|
1578
|
+
return p;
|
|
1579
|
+
}
|
|
1580
|
+
function stream(input) {
|
|
1581
|
+
const events = (async function* () {
|
|
1582
|
+
const p = await prepare(input);
|
|
1583
|
+
yield* p.run(input);
|
|
1584
|
+
})();
|
|
1585
|
+
const buffered = [];
|
|
1586
|
+
let resolveDone;
|
|
1587
|
+
const done = new Promise((r) => resolveDone = r);
|
|
1588
|
+
let failure2;
|
|
1589
|
+
const iterable = {
|
|
1590
|
+
async *[Symbol.asyncIterator]() {
|
|
1591
|
+
try {
|
|
1592
|
+
for await (const e of events) {
|
|
1593
|
+
buffered.push(e);
|
|
1594
|
+
yield e;
|
|
1595
|
+
}
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
failure2 = err;
|
|
1598
|
+
throw err;
|
|
1599
|
+
} finally {
|
|
1600
|
+
resolveDone();
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
const result = (async () => {
|
|
1605
|
+
await done;
|
|
1606
|
+
if (failure2) throw failure2;
|
|
1607
|
+
return collect(
|
|
1608
|
+
(async function* () {
|
|
1609
|
+
yield* buffered;
|
|
1610
|
+
})(),
|
|
1611
|
+
input
|
|
1612
|
+
);
|
|
1613
|
+
})();
|
|
1614
|
+
result.catch(() => {
|
|
1615
|
+
});
|
|
1616
|
+
return Object.assign(iterable, { result });
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
providers: registry,
|
|
1620
|
+
/** Installed and logged-in state for every provider, for a provider picker. */
|
|
1621
|
+
async overview() {
|
|
1622
|
+
return Promise.all(
|
|
1623
|
+
Object.values(registry).map(async (p) => {
|
|
1624
|
+
const installation = await (async () => p.detect())().catch(() => ({ installed: false, testedRange: { min: "0" } }));
|
|
1625
|
+
const auth = installation.installed ? await (async () => p.authStatus())().catch(() => void 0) : void 0;
|
|
1626
|
+
return { id: p.id, displayName: p.displayName, capabilities: p.capabilities, installation, auth };
|
|
1627
|
+
})
|
|
1628
|
+
);
|
|
1629
|
+
},
|
|
1630
|
+
models: async (id) => provider(id).models(),
|
|
1631
|
+
authStatus: async (id) => provider(id).authStatus(),
|
|
1632
|
+
login: (id, opts) => provider(id).login(opts),
|
|
1633
|
+
logout: async (id) => provider(id).logout(),
|
|
1634
|
+
update: async (id) => provider(id).update(),
|
|
1635
|
+
manifest: () => loadManifest(options.manifestUrl),
|
|
1636
|
+
stream,
|
|
1637
|
+
/** Runs one turn and resolves with the final text, usage and tool calls. */
|
|
1638
|
+
async run(input) {
|
|
1639
|
+
const s = stream(input);
|
|
1640
|
+
for await (const _ of s) {
|
|
1641
|
+
}
|
|
1642
|
+
return s.result;
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
export {
|
|
1648
|
+
loadManifest,
|
|
1649
|
+
validateSelection,
|
|
1650
|
+
createApiProviders,
|
|
1651
|
+
PROVIDERS,
|
|
1652
|
+
createFunnel
|
|
1653
|
+
};
|