cli-funnel 0.1.0 → 0.2.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/dist/bin/cli.js +3 -3
- package/dist/{chunk-2FZ64BNF.js → chunk-CJLAAJHT.js} +230 -26
- package/dist/{chunk-YP5V5YEE.js → chunk-KMG3CM2J.js} +1 -1
- package/dist/{chunk-GF7JASHO.js → chunk-TBSSPWZE.js} +20 -2
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +1 -1
- package/dist/{funnel-CoOj5wr1.d.ts → funnel-BZBIXenY.d.ts} +42 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -2
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createFunnel
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-CJLAAJHT.js";
|
|
5
5
|
import {
|
|
6
6
|
createHandler,
|
|
7
7
|
serveNode
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-TBSSPWZE.js";
|
|
9
9
|
import {
|
|
10
10
|
ACCESS_LEVELS
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-KMG3CM2J.js";
|
|
12
12
|
|
|
13
13
|
// src/bin/cli.ts
|
|
14
14
|
import { createInterface } from "readline/promises";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACCESS_LEVELS,
|
|
3
3
|
FunnelError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KMG3CM2J.js";
|
|
5
5
|
|
|
6
6
|
// src/catalog/manifest.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
@@ -126,7 +126,14 @@ var anthropic = {
|
|
|
126
126
|
method: "POST",
|
|
127
127
|
signal: input.signal,
|
|
128
128
|
headers: { "x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json" },
|
|
129
|
-
body: JSON.stringify({
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
model: input.selection.model,
|
|
131
|
+
max_tokens: input.maxOutputTokens ?? 8192,
|
|
132
|
+
stream: true,
|
|
133
|
+
...input.system ? { system: input.system } : {},
|
|
134
|
+
...input.responseSchema ? { output_config: { format: { type: "json_schema", schema: input.responseSchema.schema } } } : {},
|
|
135
|
+
messages
|
|
136
|
+
})
|
|
130
137
|
});
|
|
131
138
|
if (!res.ok) return yield { type: "error", message: `Anthropic API ${res.status}: ${await res.text()}`, code: "cli-failed" };
|
|
132
139
|
let inTok = 0;
|
|
@@ -146,6 +153,13 @@ var anthropic = {
|
|
|
146
153
|
}
|
|
147
154
|
yield { type: "usage", usage: { inputTokens: inTok, outputTokens: outTok, cachedInputTokens: cached, totalTokens: inTok + outTok } };
|
|
148
155
|
yield { type: "done", text: "", finishReason: stop === "refusal" ? "denied" : "stop" };
|
|
156
|
+
},
|
|
157
|
+
userContent(input) {
|
|
158
|
+
if (!input.attachments?.length) return input.prompt;
|
|
159
|
+
return [
|
|
160
|
+
...input.attachments.map((a) => ({ type: "image", source: { type: "base64", media_type: a.mediaType, data: a.data } })),
|
|
161
|
+
{ type: "text", text: input.prompt }
|
|
162
|
+
];
|
|
149
163
|
}
|
|
150
164
|
};
|
|
151
165
|
var openai = {
|
|
@@ -164,7 +178,15 @@ var openai = {
|
|
|
164
178
|
method: "POST",
|
|
165
179
|
signal: input.signal,
|
|
166
180
|
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
167
|
-
body: JSON.stringify({
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
model: input.selection.model,
|
|
183
|
+
// The system message is sent on every call and never stored in the session history.
|
|
184
|
+
messages: input.system ? [{ role: "system", content: input.system }, ...messages] : messages,
|
|
185
|
+
stream: true,
|
|
186
|
+
stream_options: { include_usage: true },
|
|
187
|
+
...input.maxOutputTokens ? { max_completion_tokens: input.maxOutputTokens } : {},
|
|
188
|
+
...input.responseSchema ? { response_format: { type: "json_schema", json_schema: { name: input.responseSchema.name ?? "response", schema: input.responseSchema.schema } } } : {}
|
|
189
|
+
})
|
|
168
190
|
});
|
|
169
191
|
if (!res.ok) return yield { type: "error", message: `OpenAI API ${res.status}: ${await res.text()}`, code: "cli-failed" };
|
|
170
192
|
let usage;
|
|
@@ -179,6 +201,13 @@ var openai = {
|
|
|
179
201
|
}
|
|
180
202
|
if (usage) yield { type: "usage", usage: { inputTokens: usage.prompt_tokens, outputTokens: usage.completion_tokens, totalTokens: usage.total_tokens } };
|
|
181
203
|
yield { type: "done", text: "", finishReason: stop === "content_filter" ? "denied" : "stop" };
|
|
204
|
+
},
|
|
205
|
+
userContent(input) {
|
|
206
|
+
if (!input.attachments?.length) return input.prompt;
|
|
207
|
+
return [
|
|
208
|
+
{ type: "text", text: input.prompt },
|
|
209
|
+
...input.attachments.map((a) => ({ type: "image_url", image_url: { url: `data:${a.mediaType};base64,${a.data}` } }))
|
|
210
|
+
];
|
|
182
211
|
}
|
|
183
212
|
};
|
|
184
213
|
var histories = /* @__PURE__ */ new Map();
|
|
@@ -192,7 +221,17 @@ function makeApiProvider(spec, getKey) {
|
|
|
192
221
|
id: spec.id,
|
|
193
222
|
displayName: spec.displayName,
|
|
194
223
|
binary: "fetch",
|
|
195
|
-
capabilities: {
|
|
224
|
+
capabilities: {
|
|
225
|
+
access: [],
|
|
226
|
+
effort: false,
|
|
227
|
+
contextWindow: false,
|
|
228
|
+
fast: false,
|
|
229
|
+
resume: true,
|
|
230
|
+
approvals: false,
|
|
231
|
+
images: true,
|
|
232
|
+
system: "native",
|
|
233
|
+
schema: "native"
|
|
234
|
+
},
|
|
196
235
|
detect: async () => ({ installed: true, testedRange: { min: "0" }, withinTestedRange: true }),
|
|
197
236
|
authStatus: async () => ({ loggedIn: !!(getKey() ?? process.env[spec.envVar]), method: "API key" }),
|
|
198
237
|
login() {
|
|
@@ -213,7 +252,7 @@ function makeApiProvider(spec, getKey) {
|
|
|
213
252
|
models: async () => spec.listModels(key()),
|
|
214
253
|
async *run(input) {
|
|
215
254
|
const sessionId = input.sessionId ?? randomUUID();
|
|
216
|
-
const messages = [...histories.get(sessionId) ?? [], { role: "user", content: input
|
|
255
|
+
const messages = [...histories.get(sessionId) ?? [], { role: "user", content: spec.userContent(input) }];
|
|
217
256
|
yield { type: "session", sessionId, model: input.selection.model };
|
|
218
257
|
let text = "";
|
|
219
258
|
for await (const e of spec.stream(key(), input, messages)) {
|
|
@@ -267,7 +306,7 @@ var Channel = class {
|
|
|
267
306
|
import { spawn as spawn2 } from "child_process";
|
|
268
307
|
|
|
269
308
|
// src/util/process.ts
|
|
270
|
-
import { spawn } from "child_process";
|
|
309
|
+
import { execFileSync, spawn } from "child_process";
|
|
271
310
|
import { existsSync as existsSync3, accessSync, constants } from "fs";
|
|
272
311
|
import { homedir } from "os";
|
|
273
312
|
import { delimiter, join } from "path";
|
|
@@ -279,10 +318,35 @@ var EXTRA_DIRS = [
|
|
|
279
318
|
"/opt/homebrew/bin",
|
|
280
319
|
"/usr/local/bin"
|
|
281
320
|
];
|
|
321
|
+
var MARK = "__CLI_FUNNEL_ENV__";
|
|
322
|
+
var shellPath;
|
|
323
|
+
function loginShellPath(refresh = false) {
|
|
324
|
+
if (shellPath && !refresh) return shellPath;
|
|
325
|
+
shellPath = [];
|
|
326
|
+
if (process.platform === "win32" || process.env.CLI_FUNNEL_NO_SHELL_PATH === "1") return shellPath;
|
|
327
|
+
const shell = process.env.SHELL || (process.platform === "darwin" ? "/bin/zsh" : "/bin/sh");
|
|
328
|
+
try {
|
|
329
|
+
const out = execFileSync(shell, ["-ilc", `echo ${MARK}; /usr/bin/env; echo ${MARK}`], {
|
|
330
|
+
encoding: "utf8",
|
|
331
|
+
timeout: 5e3,
|
|
332
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
333
|
+
});
|
|
334
|
+
const line = out.split(MARK)[1]?.split("\n").find((l) => l.startsWith("PATH="));
|
|
335
|
+
if (line) shellPath = line.slice(5).split(":").filter(Boolean);
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
return shellPath;
|
|
339
|
+
}
|
|
340
|
+
function searchPath() {
|
|
341
|
+
return [.../* @__PURE__ */ new Set([...(process.env.PATH ?? "").split(delimiter).filter(Boolean), ...loginShellPath(), ...EXTRA_DIRS])];
|
|
342
|
+
}
|
|
343
|
+
function childEnv(extra) {
|
|
344
|
+
return { ...process.env, PATH: searchPath().join(delimiter), ...extra };
|
|
345
|
+
}
|
|
282
346
|
function resolveBinary(name, envVar) {
|
|
283
347
|
const override = envVar ? process.env[envVar] : void 0;
|
|
284
348
|
if (override && existsSync3(override)) return override;
|
|
285
|
-
const dirs =
|
|
349
|
+
const dirs = searchPath();
|
|
286
350
|
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
287
351
|
for (const dir of dirs) {
|
|
288
352
|
for (const ext of exts) {
|
|
@@ -300,7 +364,7 @@ function exec(file, args, opts = {}) {
|
|
|
300
364
|
return new Promise((resolve, reject) => {
|
|
301
365
|
const child = spawn(file, args, {
|
|
302
366
|
cwd: opts.cwd,
|
|
303
|
-
env:
|
|
367
|
+
env: childEnv(opts.env),
|
|
304
368
|
stdio: ["pipe", "pipe", "pipe"],
|
|
305
369
|
signal: opts.signal
|
|
306
370
|
});
|
|
@@ -325,7 +389,7 @@ function exec(file, args, opts = {}) {
|
|
|
325
389
|
function spawnStream(file, args, opts = {}) {
|
|
326
390
|
const child = spawn(file, args, {
|
|
327
391
|
cwd: opts.cwd,
|
|
328
|
-
env:
|
|
392
|
+
env: childEnv(opts.env),
|
|
329
393
|
stdio: ["pipe", "pipe", "pipe"]
|
|
330
394
|
});
|
|
331
395
|
const channel = new Channel();
|
|
@@ -397,6 +461,26 @@ function compareVersions(a, b) {
|
|
|
397
461
|
return 0;
|
|
398
462
|
}
|
|
399
463
|
|
|
464
|
+
// src/providers/prompt.ts
|
|
465
|
+
function composePrompt(input, opts) {
|
|
466
|
+
const parts = [];
|
|
467
|
+
if (opts.system && input.system) parts.push(`<instructions>
|
|
468
|
+
${input.system}
|
|
469
|
+
</instructions>`);
|
|
470
|
+
parts.push(input.prompt);
|
|
471
|
+
if (opts.schema && input.responseSchema) {
|
|
472
|
+
parts.push(
|
|
473
|
+
"Reply with only a JSON value that matches this JSON Schema. No prose, no code fences.\n" + JSON.stringify(input.responseSchema.schema)
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return parts.join("\n\n");
|
|
477
|
+
}
|
|
478
|
+
function parseJsonAnswer(text) {
|
|
479
|
+
const trimmed = text.trim();
|
|
480
|
+
const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n?```$/i.exec(trimmed);
|
|
481
|
+
return JSON.parse(fenced ? fenced[1] : trimmed);
|
|
482
|
+
}
|
|
483
|
+
|
|
400
484
|
// src/providers/base.ts
|
|
401
485
|
function binaryEnvVar(binary) {
|
|
402
486
|
return `CLI_FUNNEL_${binary.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BIN`;
|
|
@@ -420,7 +504,7 @@ function spawnLogin(binary, args, readStatus, opts = {}) {
|
|
|
420
504
|
channel.push({ type: "error", message: `${binary} is not installed.` });
|
|
421
505
|
channel.end();
|
|
422
506
|
} else {
|
|
423
|
-
child = spawn2(path, args, { env:
|
|
507
|
+
child = spawn2(path, args, { env: childEnv(opts.env), signal: opts.signal, stdio: ["pipe", "pipe", "pipe"] });
|
|
424
508
|
child.stdin?.on("error", () => {
|
|
425
509
|
});
|
|
426
510
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -500,6 +584,9 @@ async function collect(events, input) {
|
|
|
500
584
|
case "usage":
|
|
501
585
|
result.usage = e.usage;
|
|
502
586
|
break;
|
|
587
|
+
case "structured":
|
|
588
|
+
result.structured = e.data;
|
|
589
|
+
break;
|
|
503
590
|
case "done":
|
|
504
591
|
if (e.text) result.text = e.text;
|
|
505
592
|
result.finishReason = e.finishReason;
|
|
@@ -509,6 +596,13 @@ async function collect(events, input) {
|
|
|
509
596
|
throw Object.assign(new Error(e.message), { code: e.code });
|
|
510
597
|
}
|
|
511
598
|
}
|
|
599
|
+
if (input.responseSchema && result.structured === void 0 && result.finishReason === "stop") {
|
|
600
|
+
try {
|
|
601
|
+
result.structured = parseJsonAnswer(result.text);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
result.structuredError = `The answer is not JSON: ${err instanceof Error ? err.message : String(err)}`;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
512
606
|
return result;
|
|
513
607
|
}
|
|
514
608
|
|
|
@@ -570,6 +664,7 @@ function parseResult(r) {
|
|
|
570
664
|
};
|
|
571
665
|
events.push({ type: "usage", usage });
|
|
572
666
|
}
|
|
667
|
+
if (r.structured_output !== void 0) events.push({ type: "structured", data: r.structured_output });
|
|
573
668
|
const denied = Array.isArray(r.permission_denials) && r.permission_denials.length > 0;
|
|
574
669
|
const finishReason = denied ? "denied" : "stop";
|
|
575
670
|
events.push({ type: "done", text, finishReason });
|
|
@@ -580,19 +675,25 @@ function parseResult(r) {
|
|
|
580
675
|
var BIN = "claude";
|
|
581
676
|
var TESTED_RANGE = { min: "2.1.0" };
|
|
582
677
|
var PERMISSION_MODE = {
|
|
678
|
+
// With every tool removed there is nothing to approve. `manual` keeps it that way if a tool slips in.
|
|
679
|
+
none: "manual",
|
|
583
680
|
supervised: "manual",
|
|
584
681
|
"accept-edits": "acceptEdits",
|
|
585
682
|
auto: "auto",
|
|
586
683
|
full: "bypassPermissions"
|
|
587
684
|
};
|
|
588
685
|
var capabilities = {
|
|
589
|
-
access: ["supervised", "accept-edits", "auto", "full"],
|
|
686
|
+
access: ["none", "supervised", "accept-edits", "auto", "full"],
|
|
590
687
|
effort: true,
|
|
591
688
|
contextWindow: false,
|
|
592
689
|
fast: false,
|
|
593
690
|
resume: true,
|
|
594
|
-
approvals: true
|
|
691
|
+
approvals: true,
|
|
692
|
+
images: true,
|
|
693
|
+
system: "native",
|
|
694
|
+
schema: "native"
|
|
595
695
|
};
|
|
696
|
+
var NO_TOOLS = ["--tools", "", "--strict-mcp-config", "--setting-sources", ""];
|
|
596
697
|
function buildArgs(input) {
|
|
597
698
|
const { selection } = input;
|
|
598
699
|
const args = [
|
|
@@ -609,10 +710,20 @@ function buildArgs(input) {
|
|
|
609
710
|
PERMISSION_MODE[selection.access]
|
|
610
711
|
];
|
|
611
712
|
if (selection.effort) args.push("--effort", selection.effort);
|
|
612
|
-
if (selection.access
|
|
713
|
+
if (selection.access === "none") args.push(...NO_TOOLS);
|
|
714
|
+
if (input.system) args.push(selection.access === "none" ? "--system-prompt" : "--append-system-prompt", input.system);
|
|
715
|
+
if (input.responseSchema) args.push("--json-schema", JSON.stringify(input.responseSchema.schema));
|
|
716
|
+
if (selection.access !== "full" && selection.access !== "none" && input.onApproval) args.push("--permission-prompt-tool", "stdio");
|
|
613
717
|
if (input.sessionId) args.push("--resume", input.sessionId);
|
|
614
718
|
return args;
|
|
615
719
|
}
|
|
720
|
+
function userContent(input) {
|
|
721
|
+
if (!input.attachments?.length) return input.prompt;
|
|
722
|
+
return [
|
|
723
|
+
...input.attachments.map((a) => ({ type: "image", source: { type: "base64", media_type: a.mediaType, data: a.data } })),
|
|
724
|
+
{ type: "text", text: input.prompt }
|
|
725
|
+
];
|
|
726
|
+
}
|
|
616
727
|
async function* run(input) {
|
|
617
728
|
const bin = findBinary(BIN);
|
|
618
729
|
if (!bin) throw new FunnelError("Claude Code is not installed.", "not-installed");
|
|
@@ -620,7 +731,7 @@ async function* run(input) {
|
|
|
620
731
|
throw new FunnelError('Access "supervised" needs an onApproval handler.', "invalid-selection");
|
|
621
732
|
}
|
|
622
733
|
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
|
|
734
|
+
proc.write(JSON.stringify({ type: "user", message: { role: "user", content: userContent(input) } }));
|
|
624
735
|
let finished = false;
|
|
625
736
|
try {
|
|
626
737
|
for await (const raw of proc.lines) {
|
|
@@ -683,6 +794,8 @@ var claudeProvider = {
|
|
|
683
794
|
// src/providers/codex/access.ts
|
|
684
795
|
function planAccess(access) {
|
|
685
796
|
switch (access) {
|
|
797
|
+
case "none":
|
|
798
|
+
return { approvalPolicy: "untrusted", sandbox: "read-only", approvalsReviewer: "user", autoAllowFileChanges: false };
|
|
686
799
|
case "supervised":
|
|
687
800
|
return { approvalPolicy: "untrusted", sandbox: "workspace-write", approvalsReviewer: "user", autoAllowFileChanges: false };
|
|
688
801
|
case "accept-edits":
|
|
@@ -892,6 +1005,60 @@ var RpcClient = class {
|
|
|
892
1005
|
}
|
|
893
1006
|
};
|
|
894
1007
|
|
|
1008
|
+
// src/providers/codex/tools.ts
|
|
1009
|
+
import { readFileSync } from "fs";
|
|
1010
|
+
import { homedir as homedir2 } from "os";
|
|
1011
|
+
import { join as join2 } from "path";
|
|
1012
|
+
var TOOL_FEATURES = [
|
|
1013
|
+
"shell_tool",
|
|
1014
|
+
"unified_exec",
|
|
1015
|
+
"apps",
|
|
1016
|
+
"plugins",
|
|
1017
|
+
"multi_agent",
|
|
1018
|
+
"multi_agent_v2",
|
|
1019
|
+
"browser_use",
|
|
1020
|
+
"computer_use",
|
|
1021
|
+
"image_generation",
|
|
1022
|
+
"hooks",
|
|
1023
|
+
"skill_search",
|
|
1024
|
+
"tool_suggest",
|
|
1025
|
+
"goals",
|
|
1026
|
+
"sleep_tool",
|
|
1027
|
+
"code_mode_host",
|
|
1028
|
+
"memories",
|
|
1029
|
+
"in_app_browser"
|
|
1030
|
+
];
|
|
1031
|
+
var SERVER_HEADER = /^\s*\[mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))\]\s*$/gm;
|
|
1032
|
+
function declaredServers(file) {
|
|
1033
|
+
let text;
|
|
1034
|
+
try {
|
|
1035
|
+
text = readFileSync(file, "utf8");
|
|
1036
|
+
} catch {
|
|
1037
|
+
return [];
|
|
1038
|
+
}
|
|
1039
|
+
return [...text.matchAll(SERVER_HEADER)].map((m) => m[1] ?? m[2]);
|
|
1040
|
+
}
|
|
1041
|
+
async function configMcpServers(bin, cwd) {
|
|
1042
|
+
const res = await exec(bin, ["mcp", "list", "--json"], { timeoutMs: 2e4 });
|
|
1043
|
+
if (res.code !== 0) throw new Error(`codex mcp list failed: ${(res.stderr || res.stdout).trim().slice(0, 200)}`);
|
|
1044
|
+
const enabled = new Set(
|
|
1045
|
+
JSON.parse(res.stdout).filter((s) => s.enabled !== false).map((s) => s.name)
|
|
1046
|
+
);
|
|
1047
|
+
const home = process.env.CODEX_HOME ?? join2(homedir2(), ".codex");
|
|
1048
|
+
const declared = [...declaredServers(join2(home, "config.toml")), ...declaredServers(join2(cwd, ".codex", "config.toml"))];
|
|
1049
|
+
return [...new Set(declared)].filter((name) => enabled.has(name));
|
|
1050
|
+
}
|
|
1051
|
+
async function noToolsConfig(bin, cwd) {
|
|
1052
|
+
const servers = await configMcpServers(bin, cwd);
|
|
1053
|
+
return Object.fromEntries([
|
|
1054
|
+
...TOOL_FEATURES.map((f) => [`features.${f}`, false]),
|
|
1055
|
+
["web_search", "disabled"],
|
|
1056
|
+
["include_apply_patch_tool", false],
|
|
1057
|
+
["tools.view_image", false],
|
|
1058
|
+
...servers.map((name) => [`mcp_servers.${name}.enabled`, false])
|
|
1059
|
+
]);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
895
1062
|
// src/providers/codex/index.ts
|
|
896
1063
|
var BINARY = "codex";
|
|
897
1064
|
var TESTED_RANGE2 = { min: "0.150.0" };
|
|
@@ -935,6 +1102,7 @@ async function* run2(input) {
|
|
|
935
1102
|
proc.child.kill("SIGTERM");
|
|
936
1103
|
};
|
|
937
1104
|
const decide = async (req) => {
|
|
1105
|
+
if (sel.access === "none") return "deny";
|
|
938
1106
|
if (req.method === "item/fileChange/requestApproval" && plan.autoAllowFileChanges) return "allow";
|
|
939
1107
|
if (sel.access === "full") return "allow";
|
|
940
1108
|
const request = toApprovalRequest(req, translator?.items ?? /* @__PURE__ */ new Map());
|
|
@@ -993,7 +1161,9 @@ async function* run2(input) {
|
|
|
993
1161
|
approvalPolicy: plan.approvalPolicy,
|
|
994
1162
|
approvalsReviewer: plan.approvalsReviewer,
|
|
995
1163
|
sandbox: plan.sandbox,
|
|
996
|
-
...sel.fast ? { serviceTier: "priority" } : {}
|
|
1164
|
+
...sel.fast ? { serviceTier: "priority" } : {},
|
|
1165
|
+
...input.system ? { developerInstructions: input.system } : {},
|
|
1166
|
+
...sel.access === "none" ? { config: await noToolsConfig(path, sel.cwd) } : {}
|
|
997
1167
|
};
|
|
998
1168
|
const started = input.sessionId ? await rpc.request("thread/resume", { threadId: input.sessionId, ...settings }) : await rpc.request("thread/start", settings);
|
|
999
1169
|
threadId = started.thread.id;
|
|
@@ -1001,8 +1171,12 @@ async function* run2(input) {
|
|
|
1001
1171
|
out.push({ type: "session", sessionId: threadId, model: started.model });
|
|
1002
1172
|
const res = await rpc.request("turn/start", {
|
|
1003
1173
|
threadId,
|
|
1004
|
-
input: [
|
|
1005
|
-
|
|
1174
|
+
input: [
|
|
1175
|
+
...(input.attachments ?? []).map((a) => ({ type: "image", url: `data:${a.mediaType};base64,${a.data}` })),
|
|
1176
|
+
{ type: "text", text: input.prompt, text_elements: [] }
|
|
1177
|
+
],
|
|
1178
|
+
...sel.effort ? { effort: sel.effort } : {},
|
|
1179
|
+
...input.responseSchema ? { outputSchema: input.responseSchema.schema } : {}
|
|
1006
1180
|
});
|
|
1007
1181
|
turnId ??= res?.turn?.id;
|
|
1008
1182
|
} catch (e) {
|
|
@@ -1021,12 +1195,15 @@ var codexProvider = {
|
|
|
1021
1195
|
displayName: "Codex",
|
|
1022
1196
|
binary: BINARY,
|
|
1023
1197
|
capabilities: {
|
|
1024
|
-
access: ["supervised", "accept-edits", "auto", "full"],
|
|
1198
|
+
access: ["none", "supervised", "accept-edits", "auto", "full"],
|
|
1025
1199
|
effort: true,
|
|
1026
1200
|
contextWindow: false,
|
|
1027
1201
|
fast: true,
|
|
1028
1202
|
resume: true,
|
|
1029
|
-
approvals: true
|
|
1203
|
+
approvals: true,
|
|
1204
|
+
images: true,
|
|
1205
|
+
system: "native",
|
|
1206
|
+
schema: "native"
|
|
1030
1207
|
},
|
|
1031
1208
|
detect: () => detectInstallation(BINARY, TESTED_RANGE2),
|
|
1032
1209
|
authStatus: authStatus2,
|
|
@@ -1271,7 +1448,7 @@ function buildArgs2(input, model) {
|
|
|
1271
1448
|
...accessFlags(selection.access),
|
|
1272
1449
|
...input.sessionId ? ["--resume", input.sessionId] : [],
|
|
1273
1450
|
"--",
|
|
1274
|
-
input
|
|
1451
|
+
composePrompt(input, { system: true, schema: true })
|
|
1275
1452
|
];
|
|
1276
1453
|
}
|
|
1277
1454
|
async function authStatus3() {
|
|
@@ -1314,7 +1491,18 @@ var agentProvider = {
|
|
|
1314
1491
|
id: "agent",
|
|
1315
1492
|
displayName: "Cursor Agent",
|
|
1316
1493
|
binary: BINARY2,
|
|
1317
|
-
|
|
1494
|
+
// No `none`: in testing, `--mode ask` with `--sandbox enabled` still ran shell commands, and only the model's own refusal stopped writes.
|
|
1495
|
+
capabilities: {
|
|
1496
|
+
access: ["auto", "full"],
|
|
1497
|
+
effort: true,
|
|
1498
|
+
contextWindow: false,
|
|
1499
|
+
fast: true,
|
|
1500
|
+
resume: true,
|
|
1501
|
+
approvals: false,
|
|
1502
|
+
images: false,
|
|
1503
|
+
system: "prompt",
|
|
1504
|
+
schema: "prompt"
|
|
1505
|
+
},
|
|
1318
1506
|
detect: () => detectInstallation(BINARY2, TESTED_RANGE3),
|
|
1319
1507
|
authStatus: authStatus3,
|
|
1320
1508
|
login: (options) => spawnLogin(BINARY2, ["login"], authStatus3, {
|
|
@@ -1336,8 +1524,8 @@ var agentProvider = {
|
|
|
1336
1524
|
|
|
1337
1525
|
// src/providers/antigravity/index.ts
|
|
1338
1526
|
import { existsSync as existsSync4 } from "fs";
|
|
1339
|
-
import { homedir as
|
|
1340
|
-
import { join as
|
|
1527
|
+
import { homedir as homedir3 } from "os";
|
|
1528
|
+
import { join as join3 } from "path";
|
|
1341
1529
|
|
|
1342
1530
|
// src/providers/antigravity/parser.ts
|
|
1343
1531
|
var EFFORTS = ["low", "medium", "high", "max"];
|
|
@@ -1438,6 +1626,7 @@ function createMapper() {
|
|
|
1438
1626
|
});
|
|
1439
1627
|
const usage = toUsage2(r.usage);
|
|
1440
1628
|
if (usage) events.push({ type: "usage", usage });
|
|
1629
|
+
if (r.structured_output !== void 0) events.push({ type: "structured", data: r.structured_output });
|
|
1441
1630
|
const text = r.response ?? "";
|
|
1442
1631
|
events.push({ type: "done", text, finishReason: denied.length && !text.trim() ? "denied" : "stop" });
|
|
1443
1632
|
return events;
|
|
@@ -1448,7 +1637,7 @@ function createMapper() {
|
|
|
1448
1637
|
}
|
|
1449
1638
|
|
|
1450
1639
|
// src/providers/antigravity/index.ts
|
|
1451
|
-
var TOKEN_FILE =
|
|
1640
|
+
var TOKEN_FILE = join3(homedir3(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
|
|
1452
1641
|
var TESTED = { min: "1.2.0" };
|
|
1453
1642
|
function toCliModel2(selection, models2 = []) {
|
|
1454
1643
|
const info = models2.find((m) => m.id === selection.model);
|
|
@@ -1458,7 +1647,8 @@ function toCliModel2(selection, models2 = []) {
|
|
|
1458
1647
|
function buildArgs3(input, models2 = []) {
|
|
1459
1648
|
const { selection } = input;
|
|
1460
1649
|
const { model, effort } = toCliModel2(selection, models2);
|
|
1461
|
-
const args = ["--print=" + input
|
|
1650
|
+
const args = ["--print=" + composePrompt(input, { system: true, schema: false }), "--output-format", "stream-json", "--model", model];
|
|
1651
|
+
if (input.responseSchema) args.push("--json-schema", JSON.stringify(input.responseSchema.schema));
|
|
1462
1652
|
if (effort) args.push("--effort", effort);
|
|
1463
1653
|
if (selection.access === "accept-edits") args.push("--mode", "accept-edits");
|
|
1464
1654
|
if (selection.access === "full") args.push("--dangerously-skip-permissions");
|
|
@@ -1527,7 +1717,18 @@ var antigravityProvider = {
|
|
|
1527
1717
|
id: "antigravity",
|
|
1528
1718
|
displayName: "Antigravity",
|
|
1529
1719
|
binary: "agy",
|
|
1530
|
-
|
|
1720
|
+
// No `none`: in testing, `--mode plan` with `--sandbox` still let a shell command write outside the workspace.
|
|
1721
|
+
capabilities: {
|
|
1722
|
+
access: ["accept-edits", "full"],
|
|
1723
|
+
effort: true,
|
|
1724
|
+
contextWindow: false,
|
|
1725
|
+
fast: false,
|
|
1726
|
+
resume: true,
|
|
1727
|
+
approvals: false,
|
|
1728
|
+
images: false,
|
|
1729
|
+
system: "prompt",
|
|
1730
|
+
schema: "native"
|
|
1731
|
+
},
|
|
1531
1732
|
detect: () => detectInstallation("agy", TESTED),
|
|
1532
1733
|
authStatus: authStatus4,
|
|
1533
1734
|
login,
|
|
@@ -1575,6 +1776,9 @@ function createFunnel(options = {}) {
|
|
|
1575
1776
|
if (input.selection.access === "supervised" && !input.onApproval) {
|
|
1576
1777
|
throw new FunnelError('access "supervised" needs an onApproval callback.', "invalid-selection");
|
|
1577
1778
|
}
|
|
1779
|
+
if (input.attachments?.length && !p.capabilities.images) {
|
|
1780
|
+
throw new FunnelError(`${p.displayName} cannot take image attachments.`, "unsupported");
|
|
1781
|
+
}
|
|
1578
1782
|
return p;
|
|
1579
1783
|
}
|
|
1580
1784
|
function stream(input) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACCESS_LEVELS,
|
|
3
3
|
FunnelError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KMG3CM2J.js";
|
|
5
5
|
|
|
6
6
|
// src/server/handler.ts
|
|
7
7
|
import { timingSafeEqual } from "crypto";
|
|
@@ -50,6 +50,15 @@ function sse(items, opts = {}) {
|
|
|
50
50
|
|
|
51
51
|
// src/server/openai.ts
|
|
52
52
|
var text = (c) => typeof c === "string" ? c : (c ?? []).map((p) => p.type === "text" ? p.text ?? "" : "").join("");
|
|
53
|
+
var IMAGE_DATA_URL = /^data:(image\/(?:png|jpeg|webp|gif));base64,(.+)$/;
|
|
54
|
+
function toAttachments(messages) {
|
|
55
|
+
const last = [...messages].reverse().find((m) => m.role === "user");
|
|
56
|
+
if (!last || typeof last.content === "string" || !last.content) return [];
|
|
57
|
+
return last.content.flatMap((p) => {
|
|
58
|
+
const m = p.type === "image_url" && p.image_url ? IMAGE_DATA_URL.exec(p.image_url.url) : null;
|
|
59
|
+
return m ? [{ type: "image", mediaType: m[1], data: m[2] }] : [];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
53
62
|
function toPrompt(messages, resumed) {
|
|
54
63
|
if (resumed) return text([...messages].reverse().find((m) => m.role === "user")?.content ?? "");
|
|
55
64
|
if (messages.length === 1) return text(messages[0].content);
|
|
@@ -86,7 +95,12 @@ async function handleOpenAI(funnel, req, path, defaults = {}, cwdAllowed = () =>
|
|
|
86
95
|
cwd,
|
|
87
96
|
access
|
|
88
97
|
},
|
|
89
|
-
|
|
98
|
+
// System messages become the run's system prompt instead of a line in the flattened chat.
|
|
99
|
+
prompt: toPrompt(body.messages.filter((m) => m.role !== "system"), !!body.x_funnel?.sessionId),
|
|
100
|
+
system: body.messages.filter((m) => m.role === "system").map((m) => text(m.content)).join("\n\n") || void 0,
|
|
101
|
+
attachments: toAttachments(body.messages),
|
|
102
|
+
responseSchema: body.response_format?.type === "json_schema" && body.response_format.json_schema?.schema ? { name: body.response_format.json_schema.name, schema: body.response_format.json_schema.schema } : void 0,
|
|
103
|
+
maxOutputTokens: body.max_completion_tokens ?? body.max_tokens,
|
|
90
104
|
sessionId: body.x_funnel?.sessionId,
|
|
91
105
|
signal: req.signal,
|
|
92
106
|
onApproval: () => "deny"
|
|
@@ -160,6 +174,10 @@ function createHandler(funnel, options = {}) {
|
|
|
160
174
|
const input = {
|
|
161
175
|
selection: body.selection,
|
|
162
176
|
prompt: body.prompt,
|
|
177
|
+
system: body.system,
|
|
178
|
+
attachments: body.attachments,
|
|
179
|
+
responseSchema: body.responseSchema,
|
|
180
|
+
maxOutputTokens: body.maxOutputTokens,
|
|
163
181
|
sessionId: body.sessionId,
|
|
164
182
|
signal: req.signal,
|
|
165
183
|
onApproval: (request) => new Promise((resolveDecision) => {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
2
|
-
export { c as ACCESS_LEVELS, d as AccessLevel, e as ApiProviderId, f as ApprovalRequest, C as Capabilities,
|
|
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-BZBIXenY.js';
|
|
2
|
+
export { c as ACCESS_LEVELS, d as AccessLevel, e as ApiProviderId, f as ApprovalRequest, g as Attachment, C as Capabilities, h as CliProviderId, E as EffortOption, i as FinishReason, j as FunnelError, I as ImageAttachment, k as Installation, l as LoginSession, m as Provider, n as ResponseSchema, o as RunResult, p as Usage } from '../funnel-BZBIXenY.js';
|
|
3
3
|
|
|
4
4
|
interface ClientOptions {
|
|
5
5
|
/** Where the handler is mounted, for example "/api/funnel" or "http://localhost:4747". */
|
|
@@ -40,7 +40,7 @@ declare function createClient(options: ClientOptions): {
|
|
|
40
40
|
ok: boolean;
|
|
41
41
|
}>;
|
|
42
42
|
/** Streams run events. The first event is `{ type: "run", runId }`, needed to answer approvals. */
|
|
43
|
-
run: (input: Pick<RunInput, "prompt" | "sessionId"> & {
|
|
43
|
+
run: (input: Pick<RunInput, "prompt" | "sessionId" | "system" | "attachments" | "responseSchema" | "maxOutputTokens"> & {
|
|
44
44
|
selection: Selection;
|
|
45
45
|
}, signal?: AbortSignal) => AsyncGenerator<RunEvent, any, any>;
|
|
46
46
|
approve: (runId: string, approvalId: string, decision: ApprovalDecision) => Promise<{
|
package/dist/client/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
type CliProviderId = "claude" | "codex" | "agent" | "antigravity";
|
|
2
2
|
type ApiProviderId = "anthropic-api" | "openai-api";
|
|
3
3
|
type ProviderId = CliProviderId | ApiProviderId;
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* How much the agent may do on the machine. Providers list the levels they can really enforce.
|
|
6
|
+
* `none` is text in, text out: no file writes, no commands, no MCP servers, no approvals.
|
|
7
|
+
*/
|
|
8
|
+
type AccessLevel = "none" | "supervised" | "accept-edits" | "auto" | "full";
|
|
6
9
|
declare const ACCESS_LEVELS: readonly AccessLevel[];
|
|
7
10
|
interface EffortOption {
|
|
8
11
|
/** Value passed to the CLI, for example "high". */
|
|
@@ -32,6 +35,12 @@ interface Capabilities {
|
|
|
32
35
|
fast: boolean;
|
|
33
36
|
resume: boolean;
|
|
34
37
|
approvals: boolean;
|
|
38
|
+
/** Accepts image attachments. */
|
|
39
|
+
images: boolean;
|
|
40
|
+
/** `native` when the CLI or API takes a system prompt. `prompt` means cli-funnel puts it in front of the prompt. */
|
|
41
|
+
system: "native" | "prompt";
|
|
42
|
+
/** `native` when the CLI or API enforces `responseSchema`. `prompt` means the schema is only asked for in the prompt. */
|
|
43
|
+
schema: "native" | "prompt";
|
|
35
44
|
}
|
|
36
45
|
/** The whole UI state in one serializable object. Hardcode it or let the UI produce it. */
|
|
37
46
|
interface Selection {
|
|
@@ -53,6 +62,19 @@ interface Usage {
|
|
|
53
62
|
/** Set only when the CLI reports it. Subscription runs often do not. */
|
|
54
63
|
costUsd?: number;
|
|
55
64
|
}
|
|
65
|
+
/** An image sent with the prompt. `data` is base64 without a `data:` prefix. */
|
|
66
|
+
interface ImageAttachment {
|
|
67
|
+
type: "image";
|
|
68
|
+
mediaType: "image/png" | "image/jpeg" | "image/webp" | "image/gif";
|
|
69
|
+
data: string;
|
|
70
|
+
}
|
|
71
|
+
type Attachment = ImageAttachment;
|
|
72
|
+
/** A JSON Schema for the final answer. Anthropic and Codex need `additionalProperties: false` on every object. */
|
|
73
|
+
interface ResponseSchema {
|
|
74
|
+
/** Short name, used by APIs that label schemas. Default "response". */
|
|
75
|
+
name?: string;
|
|
76
|
+
schema: Record<string, unknown>;
|
|
77
|
+
}
|
|
56
78
|
type FinishReason = "stop" | "cancelled" | "error" | "denied";
|
|
57
79
|
interface ApprovalRequest {
|
|
58
80
|
id: string;
|
|
@@ -90,6 +112,11 @@ type FunnelEvent = {
|
|
|
90
112
|
} | {
|
|
91
113
|
type: "usage";
|
|
92
114
|
usage: Usage;
|
|
115
|
+
}
|
|
116
|
+
/** The parsed answer when the provider reports structured output itself. */
|
|
117
|
+
| {
|
|
118
|
+
type: "structured";
|
|
119
|
+
data: unknown;
|
|
93
120
|
} | {
|
|
94
121
|
type: "done";
|
|
95
122
|
text: string;
|
|
@@ -116,10 +143,22 @@ interface RunResult {
|
|
|
116
143
|
}[];
|
|
117
144
|
/** Tool calls the CLI refused because nobody could approve them. */
|
|
118
145
|
deniedActions: string[];
|
|
146
|
+
/** The parsed answer when `responseSchema` was set. Not validated against the schema: do that yourself. */
|
|
147
|
+
structured?: unknown;
|
|
148
|
+
/** Why `structured` is missing when `responseSchema` was set, for example text that is not JSON. */
|
|
149
|
+
structuredError?: string;
|
|
119
150
|
}
|
|
120
151
|
interface RunInput {
|
|
121
152
|
selection: Selection;
|
|
122
153
|
prompt: string;
|
|
154
|
+
/** Instructions for the whole run. See `capabilities.system` for how each provider applies them. */
|
|
155
|
+
system?: string;
|
|
156
|
+
/** Images sent with the prompt. Providers without `capabilities.images` refuse the run. */
|
|
157
|
+
attachments?: Attachment[];
|
|
158
|
+
/** Ask for a JSON answer that matches this schema. The result carries it in `structured`. */
|
|
159
|
+
responseSchema?: ResponseSchema;
|
|
160
|
+
/** Upper bound on output tokens. API providers only. CLIs set their own limit. */
|
|
161
|
+
maxOutputTokens?: number;
|
|
123
162
|
/** Continue an earlier conversation. Use the `sessionId` from a previous result. */
|
|
124
163
|
sessionId?: string;
|
|
125
164
|
signal?: AbortSignal;
|
|
@@ -261,4 +300,4 @@ declare function createFunnel(options?: FunnelOptions): {
|
|
|
261
300
|
};
|
|
262
301
|
type Funnel = ReturnType<typeof createFunnel>;
|
|
263
302
|
|
|
264
|
-
export { type AuthStatus as A, type Capabilities as C, type EffortOption as E, type FunnelEvent as F, type
|
|
303
|
+
export { type AuthStatus as A, type Capabilities as C, type EffortOption as E, type FunnelEvent as F, type ImageAttachment as I, type LoginEvent as L, type ModelInfo as M, type ProviderOverview as P, type RunInput as R, type Selection as S, type UpdateResult as U, type ProviderId as a, type ApprovalDecision as b, ACCESS_LEVELS as c, type AccessLevel as d, type ApiProviderId as e, type ApprovalRequest as f, type Attachment as g, type CliProviderId as h, type FinishReason as i, FunnelError as j, type Installation as k, type LoginSession as l, type Provider as m, type ResponseSchema as n, type RunResult as o, type Usage as p, type Funnel as q, type FunnelOptions as r, type ManifestModel as s, type ModelManifest as t, type RunStream as u, createFunnel as v, loadManifest as w };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as Selection,
|
|
2
|
-
export { c as ACCESS_LEVELS, d as AccessLevel, b as ApprovalDecision, f as ApprovalRequest, A as AuthStatus, C as Capabilities,
|
|
1
|
+
import { S as Selection, m as Provider, M as ModelInfo, a as ProviderId, e as ApiProviderId } from './funnel-BZBIXenY.js';
|
|
2
|
+
export { c as ACCESS_LEVELS, d as AccessLevel, b as ApprovalDecision, f as ApprovalRequest, g as Attachment, A as AuthStatus, C as Capabilities, h as CliProviderId, E as EffortOption, i as FinishReason, q as Funnel, j as FunnelError, F as FunnelEvent, r as FunnelOptions, I as ImageAttachment, k as Installation, L as LoginEvent, l as LoginSession, s as ManifestModel, t as ModelManifest, P as ProviderOverview, n as ResponseSchema, R as RunInput, o as RunResult, u as RunStream, U as UpdateResult, p as Usage, v as createFunnel, w as loadManifest } from './funnel-BZBIXenY.js';
|
|
3
3
|
|
|
4
4
|
/** Throws FunnelError("invalid-selection") when the selection asks for something the provider cannot do. */
|
|
5
5
|
declare function validateSelection(selection: Selection, provider: Provider, models?: ModelInfo[]): void;
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
createFunnel,
|
|
5
5
|
loadManifest,
|
|
6
6
|
validateSelection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-CJLAAJHT.js";
|
|
8
8
|
import {
|
|
9
9
|
ACCESS_LEVELS,
|
|
10
10
|
FunnelError
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-KMG3CM2J.js";
|
|
12
12
|
export {
|
|
13
13
|
ACCESS_LEVELS,
|
|
14
14
|
FunnelError,
|
package/dist/server/index.d.ts
CHANGED
package/dist/server/index.js
CHANGED