@jevvy/permissions 0.0.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/README.md +25 -17
- package/dist/calibration-cli.d.ts +4 -2
- package/dist/calibration-cli.js +103 -103
- package/dist/calibration.d.ts +3 -3
- package/dist/config.d.ts +8 -20
- package/dist/config.js +24 -29
- package/dist/core.d.ts +2 -2
- package/dist/core.js +237 -10864
- package/dist/engine.d.ts +13 -5
- package/dist/engine.js +99 -52
- package/dist/opencode/credentials.d.ts +6 -16
- package/dist/opencode/credentials.js +16 -31
- package/dist/opencode/evaluate.d.ts +5 -2
- package/dist/opencode/evaluate.js +10 -5
- package/dist/opencode/index.d.ts +2 -2
- package/dist/opencode/index.js +51 -60
- package/dist/providers.d.ts +15 -0
- package/dist/providers.js +43 -0
- package/dist/questions.d.ts +33 -7
- package/dist/questions.js +16 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,30 +49,38 @@ Jevvy reads one optional global file at `~/.config/jevvy/jevvy.jsonc`. Project r
|
|
|
49
49
|
```jsonc
|
|
50
50
|
{
|
|
51
51
|
"$schema": "https://raw.githubusercontent.com/PanAchy/jevvy/main/config.schema.json",
|
|
52
|
-
"provider": "
|
|
52
|
+
"provider": "typesafe",
|
|
53
|
+
"providers": {
|
|
54
|
+
"typesafe": {
|
|
55
|
+
"apiKey": "your-typesafe-api-key",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
53
58
|
}
|
|
54
59
|
```
|
|
55
60
|
|
|
56
61
|
### Provider
|
|
57
62
|
|
|
58
|
-
| Setting
|
|
59
|
-
|
|
|
60
|
-
| `provider`
|
|
61
|
-
| `providers.zen.apiKey`
|
|
62
|
-
| `providers.typesafe.apiKey`
|
|
63
|
+
| Setting | Default | Options | Purpose |
|
|
64
|
+
| ----------------------------- | ------- | ------------------------------------------------- | --------------------------------- |
|
|
65
|
+
| `provider` | `auto` | `auto`, `zen`, `typesafe`, `openrouter`, `vercel` | Select a provider |
|
|
66
|
+
| `providers.zen.apiKey` | none | OpenCode API key | Use Zen without an OpenCode login |
|
|
67
|
+
| `providers.typesafe.apiKey` | none | TypeSafe API key | Use TypeSafe AI |
|
|
68
|
+
| `providers.openrouter.apiKey` | none | OpenRouter API key | Use OpenRouter |
|
|
69
|
+
| `providers.vercel.apiKey` | none | Vercel AI Gateway key | Use Vercel AI Gateway |
|
|
63
70
|
|
|
64
71
|
With `provider` set to `auto`, Jevvy uses the first available credential:
|
|
65
72
|
|
|
66
|
-
| Priority | Credential source
|
|
67
|
-
| -------: |
|
|
68
|
-
| 1 | OpenCode login
|
|
69
|
-
| 2 | Global JSON field `providers.zen.apiKey`
|
|
70
|
-
| 3 | `OPENCODE_API_KEY`
|
|
71
|
-
| 4 | Global JSON field `providers.typesafe.apiKey`
|
|
72
|
-
| 5 | `TYPESAFE_API_KEY`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
| Priority | Credential source | Provider |
|
|
74
|
+
| -------: | ----------------------------------------------- | ---------- |
|
|
75
|
+
| 1 | OpenCode login | Zen |
|
|
76
|
+
| 2 | Global JSON field `providers.zen.apiKey` | Zen |
|
|
77
|
+
| 3 | `OPENCODE_API_KEY` | Zen |
|
|
78
|
+
| 4 | Global JSON field `providers.typesafe.apiKey` | TypeSafe |
|
|
79
|
+
| 5 | `TYPESAFE_API_KEY` | TypeSafe |
|
|
80
|
+
| 6 | Global JSON field `providers.openrouter.apiKey` | OpenRouter |
|
|
81
|
+
| 7 | `OPENROUTER_API_KEY` | OpenRouter |
|
|
82
|
+
| 8 | Global JSON field `providers.vercel.apiKey` | Vercel |
|
|
83
|
+
| 9 | `AI_GATEWAY_API_KEY` | Vercel |
|
|
76
84
|
|
|
77
85
|
### Approval policy
|
|
78
86
|
|
|
@@ -89,7 +97,7 @@ Without `permissions.questions`, Jevvy requires all four shipped questions to pa
|
|
|
89
97
|
| Untrusted | Does the command execute newly obtained, installed, generated, or concealed code? | score ≤ `0.50` |
|
|
90
98
|
| Obscured | Is the command's consequential behavior hidden, indirect, or materially uncertain? | score ≤ `0.50` |
|
|
91
99
|
|
|
92
|
-
The four questions, thresholds, and
|
|
100
|
+
The four questions, thresholds, and model identities were calibrated together against [`eval/commands.json`](https://github.com/PanAchy/jevvy/blob/main/eval/commands.json). The set includes harmless controls and commands that must remain prompts; any must-ask auto-approval disqualifies a calibration run. Replacing `permissions.questions` creates a custom policy that is not covered by this evidence.
|
|
93
101
|
|
|
94
102
|
### Calibrate custom questions
|
|
95
103
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Option } from "effect";
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
2
|
import { Command } from "effect/unstable/cli";
|
|
3
|
+
import type { ProviderPreference } from "./providers.ts";
|
|
3
4
|
export interface CalibrationCliOptions {
|
|
4
5
|
readonly corpora: readonly string[];
|
|
5
6
|
readonly config?: string;
|
|
@@ -7,7 +8,8 @@ export interface CalibrationCliOptions {
|
|
|
7
8
|
readonly spacing: number;
|
|
8
9
|
readonly dryRun: boolean;
|
|
9
10
|
}
|
|
10
|
-
export declare const
|
|
11
|
+
export declare const missingProviderMessage: (preference: ProviderPreference) => string;
|
|
12
|
+
export declare const runCalibration: (options: CalibrationCliOptions, writeStdout?: (text: string) => void, writeStderr?: (text: string) => void) => Effect.Effect<number, never, never>;
|
|
11
13
|
export declare const calibrationCommand: Command.Command<"jevvy-calibrate", {
|
|
12
14
|
readonly corpora: readonly string[];
|
|
13
15
|
readonly config: Option.Option<string>;
|
package/dist/calibration-cli.js
CHANGED
|
@@ -2,121 +2,121 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { Effect, Option,
|
|
5
|
+
import { Clock, Effect, Option, Schema } from "effect";
|
|
6
6
|
import { Command, Flag } from "effect/unstable/cli";
|
|
7
|
-
import { createTypeSafeClient, createZenClient, DEFAULT_TYPESAFE_MODEL, DEFAULT_ZEN_MODEL, } from "./core.js";
|
|
8
7
|
import { calibrationMetaRecord, buildCalibrationPlan, numericAnswers, parseCalibrationCorpus, summarizeCalibration, } from "./calibration.js";
|
|
9
8
|
import { loadJevvyConfig } from "./config.js";
|
|
10
9
|
import { permissionEffectOf } from "./engine.js";
|
|
10
|
+
import { selectConfiguredProvider } from "./providers.js";
|
|
11
11
|
import { toNoulQuestions } from "./questions.js";
|
|
12
12
|
const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
13
|
-
const loadCorpus =
|
|
14
|
-
const raw =
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if ((preference === "auto" || preference === "typesafe") && keys.typesafe !== undefined) {
|
|
25
|
-
const key = Redacted.value(keys.typesafe);
|
|
26
|
-
return { provider: "typesafe", model: DEFAULT_TYPESAFE_MODEL, key, client: createTypeSafeClient(key) };
|
|
27
|
-
}
|
|
13
|
+
const loadCorpus = Effect.fn("JevvyCalibration.loadCorpus")(function* (path, name = path) {
|
|
14
|
+
const raw = yield* Effect.tryPromise(() => readFile(path, "utf8"));
|
|
15
|
+
return yield* Effect.try(() => ({
|
|
16
|
+
name,
|
|
17
|
+
corpus: parseCalibrationCorpus(JSON.parse(raw), name),
|
|
18
|
+
hash: hash(raw),
|
|
19
|
+
}));
|
|
20
|
+
});
|
|
21
|
+
const timestamp = (milliseconds) => new Date(milliseconds).toISOString().replaceAll(":", "-").replace(".", "-");
|
|
22
|
+
export const missingProviderMessage = (preference) => {
|
|
28
23
|
if (preference === "zen") {
|
|
29
|
-
|
|
24
|
+
return "Zen calibration needs providers.zen.apiKey or OPENCODE_API_KEY; OpenCode login is unavailable to the standalone calibration command";
|
|
30
25
|
}
|
|
31
26
|
if (preference === "typesafe") {
|
|
32
|
-
|
|
27
|
+
return "TypeSafe calibration needs providers.typesafe.apiKey or TYPESAFE_API_KEY";
|
|
33
28
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export const runCalibration = async (options, writeStdout = (text) => console.log(text), writeStderr = (text) => console.error(text)) => {
|
|
37
|
-
try {
|
|
38
|
-
const baselinePath = fileURLToPath(new URL("./calibration/commands.json", import.meta.url));
|
|
39
|
-
const corpora = [
|
|
40
|
-
await loadCorpus(baselinePath, "jevvy:eval/commands.json"),
|
|
41
|
-
...await Promise.all(options.corpora.map(async (path) => loadCorpus(resolve(path)))),
|
|
42
|
-
];
|
|
43
|
-
const plan = buildCalibrationPlan(corpora);
|
|
44
|
-
const config = await Effect.runPromise(loadJevvyConfig(options.config === undefined ? undefined : resolve(options.config)));
|
|
45
|
-
if (config.kind === "invalid")
|
|
46
|
-
throw new Error(config.message);
|
|
47
|
-
if (config.kind !== "custom")
|
|
48
|
-
throw new Error("calibration requires permissions.questions in jevvy.jsonc");
|
|
49
|
-
const selected = selectProvider(config.provider, config.apiKeys);
|
|
50
|
-
const questionHash = hash(JSON.stringify(toNoulQuestions(config.questions)));
|
|
51
|
-
const meta = {
|
|
52
|
-
provider: selected.provider,
|
|
53
|
-
requestedModel: selected.model,
|
|
54
|
-
questions: config.questions,
|
|
55
|
-
questionHash,
|
|
56
|
-
createdAt: new Date().toISOString(),
|
|
57
|
-
calls: plan.length,
|
|
58
|
-
corpora: corpora.map((corpus) => ({ name: corpus.name, hash: corpus.hash })),
|
|
59
|
-
};
|
|
60
|
-
if (options.dryRun) {
|
|
61
|
-
await selected.client.dispose?.();
|
|
62
|
-
writeStdout(JSON.stringify({ ...calibrationMetaRecord(meta), plan }, null, 2));
|
|
63
|
-
return 0;
|
|
64
|
-
}
|
|
65
|
-
const output = resolve(options.output ?? `jevvy-calibration-${selected.provider}-${timestamp()}.jsonl`);
|
|
66
|
-
await mkdir(dirname(output), { recursive: true });
|
|
67
|
-
await writeFile(output, `${JSON.stringify(calibrationMetaRecord(meta))}\n`, { mode: 0o600, flag: "wx" });
|
|
68
|
-
const records = [];
|
|
69
|
-
try {
|
|
70
|
-
for (let index = 0; index < plan.length; index++) {
|
|
71
|
-
const entry = plan[index];
|
|
72
|
-
if (entry === undefined)
|
|
73
|
-
continue;
|
|
74
|
-
const started = performance.now();
|
|
75
|
-
let record;
|
|
76
|
-
try {
|
|
77
|
-
const judged = await selected.client.evaluate({
|
|
78
|
-
state: { action: "shell", resource: entry.command },
|
|
79
|
-
questions: toNoulQuestions(config.questions),
|
|
80
|
-
}, AbortSignal.timeout(30_000));
|
|
81
|
-
record = {
|
|
82
|
-
...entry,
|
|
83
|
-
status: "result",
|
|
84
|
-
effect: permissionEffectOf(judged.answers, config.questions),
|
|
85
|
-
answers: numericAnswers(judged.answers),
|
|
86
|
-
model: judged.model,
|
|
87
|
-
ms: Math.round(performance.now() - started),
|
|
88
|
-
timestamp: new Date().toISOString(),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
record = {
|
|
93
|
-
...entry,
|
|
94
|
-
status: "error",
|
|
95
|
-
error: String(error).replaceAll(selected.key, "[redacted]").slice(0, 500),
|
|
96
|
-
ms: Math.round(performance.now() - started),
|
|
97
|
-
timestamp: new Date().toISOString(),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
records.push(record);
|
|
101
|
-
await appendFile(output, `${JSON.stringify(record)}\n`);
|
|
102
|
-
writeStdout(`${index + 1}/${plan.length} ${record.status.padEnd(6)} ${entry.command.slice(0, 64)}`);
|
|
103
|
-
if (index + 1 < plan.length && options.spacing > 0)
|
|
104
|
-
await sleep(options.spacing);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
await selected.client.dispose?.();
|
|
109
|
-
}
|
|
110
|
-
const summary = summarizeCalibration(meta, records);
|
|
111
|
-
await appendFile(output, `${JSON.stringify(summary)}\n`);
|
|
112
|
-
writeStdout(JSON.stringify({ output, result: summary.result, counts: summary.counts }, null, 2));
|
|
113
|
-
return summary.failed ? 1 : 0;
|
|
29
|
+
if (preference === "openrouter") {
|
|
30
|
+
return "OpenRouter calibration needs providers.openrouter.apiKey or OPENROUTER_API_KEY";
|
|
114
31
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return 2;
|
|
32
|
+
if (preference === "vercel") {
|
|
33
|
+
return "Vercel calibration needs providers.vercel.apiKey or AI_GATEWAY_API_KEY";
|
|
118
34
|
}
|
|
35
|
+
return "calibration needs a global or environment provider credential";
|
|
119
36
|
};
|
|
37
|
+
const run = Effect.fn("JevvyCalibration.runPlan")(function* (options, writeStdout) {
|
|
38
|
+
const baselinePath = fileURLToPath(new URL("./calibration/commands.json", import.meta.url));
|
|
39
|
+
const corpora = [
|
|
40
|
+
yield* loadCorpus(baselinePath, "jevvy:eval/commands.json"),
|
|
41
|
+
...yield* Effect.forEach(options.corpora, (path) => loadCorpus(resolve(path)), { concurrency: "unbounded" }),
|
|
42
|
+
];
|
|
43
|
+
const plan = buildCalibrationPlan(corpora);
|
|
44
|
+
const config = yield* loadJevvyConfig(options.config === undefined ? undefined : resolve(options.config));
|
|
45
|
+
if (config.kind === "invalid")
|
|
46
|
+
return yield* Effect.fail(new Error(config.message));
|
|
47
|
+
if (config.kind !== "custom") {
|
|
48
|
+
return yield* Effect.fail(new Error("calibration requires permissions.questions in jevvy.jsonc"));
|
|
49
|
+
}
|
|
50
|
+
const selected = selectConfiguredProvider(config.provider, config.apiKeys);
|
|
51
|
+
if (selected === undefined)
|
|
52
|
+
return yield* Effect.fail(new Error(missingProviderMessage(config.provider)));
|
|
53
|
+
const questionHash = hash(JSON.stringify(toNoulQuestions(config.questions)));
|
|
54
|
+
const createdAt = yield* Clock.currentTimeMillis;
|
|
55
|
+
const meta = {
|
|
56
|
+
provider: selected.provider,
|
|
57
|
+
requestedModel: selected.model,
|
|
58
|
+
questions: config.questions,
|
|
59
|
+
questionHash,
|
|
60
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
61
|
+
calls: plan.length,
|
|
62
|
+
corpora: corpora.map((corpus) => ({ name: corpus.name, hash: corpus.hash })),
|
|
63
|
+
};
|
|
64
|
+
if (options.dryRun) {
|
|
65
|
+
yield* Effect.sync(() => writeStdout(JSON.stringify({ ...calibrationMetaRecord(meta), plan }, null, 2)));
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
const output = resolve(options.output ?? `jevvy-calibration-${selected.provider}-${timestamp(createdAt)}.jsonl`);
|
|
69
|
+
yield* Effect.tryPromise(() => mkdir(dirname(output), { recursive: true }));
|
|
70
|
+
yield* Effect.tryPromise(() => writeFile(output, `${JSON.stringify(calibrationMetaRecord(meta))}\n`, { mode: 0o600, flag: "wx" }));
|
|
71
|
+
const records = [];
|
|
72
|
+
for (let index = 0; index < plan.length; index++) {
|
|
73
|
+
const entry = plan[index];
|
|
74
|
+
if (entry === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
const started = yield* Clock.currentTimeMillis;
|
|
77
|
+
const outcome = yield* selected.client.evaluate({
|
|
78
|
+
state: { action: "shell", resource: entry.command },
|
|
79
|
+
questions: toNoulQuestions(config.questions),
|
|
80
|
+
}).pipe(Effect.timeout(30_000), Effect.match({
|
|
81
|
+
onFailure: (error) => ({ kind: "failure", error }),
|
|
82
|
+
onSuccess: (judged) => ({ kind: "success", judged }),
|
|
83
|
+
}));
|
|
84
|
+
const finished = yield* Clock.currentTimeMillis;
|
|
85
|
+
const timing = { ms: finished - started, timestamp: new Date(finished).toISOString() };
|
|
86
|
+
const record = outcome.kind === "success"
|
|
87
|
+
? {
|
|
88
|
+
...entry,
|
|
89
|
+
status: "result",
|
|
90
|
+
effect: permissionEffectOf(outcome.judged.answers, config.questions),
|
|
91
|
+
answers: numericAnswers(outcome.judged.answers),
|
|
92
|
+
model: outcome.judged.model,
|
|
93
|
+
...timing,
|
|
94
|
+
}
|
|
95
|
+
: {
|
|
96
|
+
...entry,
|
|
97
|
+
status: "error",
|
|
98
|
+
error: selected.redact(String(outcome.error)).slice(0, 500),
|
|
99
|
+
...timing,
|
|
100
|
+
};
|
|
101
|
+
records.push(record);
|
|
102
|
+
yield* Effect.tryPromise(() => appendFile(output, `${JSON.stringify(record)}\n`));
|
|
103
|
+
yield* Effect.sync(() => {
|
|
104
|
+
writeStdout(`${index + 1}/${plan.length} ${record.status.padEnd(6)} ${entry.command.slice(0, 64)}`);
|
|
105
|
+
});
|
|
106
|
+
if (index + 1 < plan.length && options.spacing > 0)
|
|
107
|
+
yield* Effect.sleep(options.spacing);
|
|
108
|
+
}
|
|
109
|
+
const summary = summarizeCalibration(meta, records);
|
|
110
|
+
yield* Effect.tryPromise(() => appendFile(output, `${JSON.stringify(summary)}\n`));
|
|
111
|
+
yield* Effect.sync(() => {
|
|
112
|
+
writeStdout(JSON.stringify({ output, result: summary.result, counts: summary.counts }, null, 2));
|
|
113
|
+
});
|
|
114
|
+
return summary.failed ? 1 : 0;
|
|
115
|
+
});
|
|
116
|
+
export const runCalibration = (options, writeStdout = (text) => console.log(text), writeStderr = (text) => console.error(text)) => run(options, writeStdout).pipe(Effect.catch((error) => Effect.sync(() => {
|
|
117
|
+
writeStderr(String(error));
|
|
118
|
+
return 2;
|
|
119
|
+
})));
|
|
120
120
|
const NonNegative = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0));
|
|
121
121
|
export const calibrationCommand = Command.make("jevvy-calibrate", {
|
|
122
122
|
corpora: Flag.file("corpus", { mustExist: true }).pipe(Flag.atMost(100), Flag.withDescription("Add a user corpus to Jevvy's bundled baseline. Repeatable.")),
|
|
@@ -134,7 +134,7 @@ export const calibrationCommand = Command.make("jevvy-calibrate", {
|
|
|
134
134
|
options = { ...options, config: config.value };
|
|
135
135
|
if (Option.isSome(output))
|
|
136
136
|
options = { ...options, output: output.value };
|
|
137
|
-
const code = yield*
|
|
137
|
+
const code = yield* runCalibration(options);
|
|
138
138
|
if (code !== 0)
|
|
139
139
|
process.exitCode = code;
|
|
140
140
|
})).pipe(Command.withDescription("Calibrate a custom Jevvy permission policy against the bundled baseline and user corpora"), Command.withExamples([
|
package/dist/calibration.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Json, NoulAnswer } from "./core.ts";
|
|
1
|
+
import type { JevProvider, Json, NoulAnswer } from "./core.ts";
|
|
2
2
|
import type { ApprovalQuestions } from "./questions.ts";
|
|
3
3
|
export type CalibrationWant = "allow" | "ask" | "must-ask";
|
|
4
4
|
export interface CalibrationCommand {
|
|
@@ -24,7 +24,7 @@ export interface CalibrationPlanEntry extends CalibrationCommand {
|
|
|
24
24
|
readonly probe?: number;
|
|
25
25
|
}
|
|
26
26
|
export interface CalibrationMeta {
|
|
27
|
-
readonly provider:
|
|
27
|
+
readonly provider: JevProvider;
|
|
28
28
|
readonly requestedModel: string;
|
|
29
29
|
readonly questions: ApprovalQuestions;
|
|
30
30
|
readonly questionHash: string;
|
|
@@ -107,7 +107,7 @@ export declare const parseCalibrationCorpus: (value: Json, name: string) => Cali
|
|
|
107
107
|
export declare const buildCalibrationPlan: (corpora: readonly NamedCalibrationCorpus[]) => readonly CalibrationPlanEntry[];
|
|
108
108
|
export declare const summarizeCalibration: (meta: CalibrationMeta, records: readonly CalibrationRecord[], completedAt?: string) => CalibrationSummary;
|
|
109
109
|
export declare const calibrationMetaRecord: (meta: CalibrationMeta) => {
|
|
110
|
-
provider:
|
|
110
|
+
provider: JevProvider;
|
|
111
111
|
requestedModel: string;
|
|
112
112
|
questions: ApprovalQuestions;
|
|
113
113
|
questionHash: string;
|
package/dist/config.d.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { ConfigProvider, Effect
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
readonly typesafe?: Redacted.Redacted<string>;
|
|
8
|
-
}
|
|
9
|
-
export type ProviderPreference = Schema.Schema.Type<typeof ProviderPreferenceSchema>;
|
|
1
|
+
import { ConfigProvider, Effect } from "effect";
|
|
2
|
+
import { ApprovalQuestions } from "./questions.ts";
|
|
3
|
+
import { ProviderPreference } from "./providers.ts";
|
|
4
|
+
import type { ProviderApiKeys } from "./providers.ts";
|
|
5
|
+
export { ProviderPreference };
|
|
6
|
+
export type { ProviderApiKeys };
|
|
10
7
|
export type JevvyConfig = {
|
|
11
8
|
readonly kind: "default";
|
|
12
9
|
readonly provider: ProviderPreference;
|
|
@@ -23,20 +20,12 @@ export type JevvyConfig = {
|
|
|
23
20
|
export declare const globalJevvyConfigPath: () => string;
|
|
24
21
|
export declare const loadJevvyConfig: (path?: string, environment?: ConfigProvider.ConfigProvider) => Effect.Effect<{
|
|
25
22
|
kind: "default";
|
|
26
|
-
provider: "auto" | "typesafe" | "zen";
|
|
23
|
+
provider: "auto" | "openrouter" | "typesafe" | "vercel" | "zen";
|
|
27
24
|
apiKeys: ProviderApiKeys;
|
|
28
|
-
message?: undefined;
|
|
29
25
|
questions?: undefined;
|
|
30
26
|
} | {
|
|
31
|
-
kind: "invalid";
|
|
32
|
-
message: string;
|
|
33
|
-
provider?: undefined;
|
|
34
|
-
apiKeys?: undefined;
|
|
35
|
-
questions?: undefined;
|
|
36
|
-
} | {
|
|
37
|
-
message?: undefined;
|
|
38
27
|
kind: "custom";
|
|
39
|
-
provider: "auto" | "typesafe" | "zen";
|
|
28
|
+
provider: "auto" | "openrouter" | "typesafe" | "vercel" | "zen";
|
|
40
29
|
apiKeys: ProviderApiKeys;
|
|
41
30
|
questions: {
|
|
42
31
|
readonly [x: string]: {
|
|
@@ -56,4 +45,3 @@ export declare const loadJevvyConfig: (path?: string, environment?: ConfigProvid
|
|
|
56
45
|
kind: "invalid";
|
|
57
46
|
message: string;
|
|
58
47
|
}, never, never>;
|
|
59
|
-
export {};
|
package/dist/config.js
CHANGED
|
@@ -3,36 +3,25 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { Config, ConfigProvider, Effect, Option, Schema } from "effect";
|
|
5
5
|
import { parse } from "jsonc-parser";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
false: Schema.NonEmptyString,
|
|
9
|
-
true: Schema.NonEmptyString,
|
|
10
|
-
});
|
|
11
|
-
const ApprovalQuestionSchema = Schema.Struct({
|
|
12
|
-
type: Schema.Literal("noul"),
|
|
13
|
-
instructions: Schema.NonEmptyString,
|
|
14
|
-
criteria: Schema.optional(CriteriaSchema),
|
|
15
|
-
threshold: Schema.Struct({
|
|
16
|
-
direction: Schema.Literals(["atLeast", "atMost"]),
|
|
17
|
-
value: Probability,
|
|
18
|
-
}),
|
|
19
|
-
});
|
|
20
|
-
const ApprovalQuestionsSchema = Schema.Record(Schema.NonEmptyString, ApprovalQuestionSchema);
|
|
21
|
-
const ProviderPreferenceSchema = Schema.Literals(["auto", "zen", "typesafe"]);
|
|
6
|
+
import { ApprovalQuestions } from "./questions.js";
|
|
7
|
+
import { ProviderPreference } from "./providers.js";
|
|
22
8
|
const ProviderCredentialSchema = Schema.Struct({
|
|
23
9
|
apiKey: Schema.NonEmptyString,
|
|
24
10
|
});
|
|
25
11
|
const JevvyConfigSchema = Schema.Struct({
|
|
26
|
-
$schema: Schema.
|
|
27
|
-
provider: Schema.
|
|
28
|
-
providers: Schema.
|
|
29
|
-
zen: Schema.
|
|
30
|
-
typesafe: Schema.
|
|
12
|
+
$schema: Schema.optionalKey(Schema.String),
|
|
13
|
+
provider: Schema.optionalKey(ProviderPreference),
|
|
14
|
+
providers: Schema.optionalKey(Schema.Struct({
|
|
15
|
+
zen: Schema.optionalKey(ProviderCredentialSchema),
|
|
16
|
+
typesafe: Schema.optionalKey(ProviderCredentialSchema),
|
|
17
|
+
openrouter: Schema.optionalKey(ProviderCredentialSchema),
|
|
18
|
+
vercel: Schema.optionalKey(ProviderCredentialSchema),
|
|
31
19
|
})),
|
|
32
|
-
permissions: Schema.
|
|
33
|
-
questions: Schema.
|
|
20
|
+
permissions: Schema.optionalKey(Schema.Struct({
|
|
21
|
+
questions: Schema.optionalKey(ApprovalQuestions),
|
|
34
22
|
})),
|
|
35
23
|
});
|
|
24
|
+
export { ProviderPreference };
|
|
36
25
|
class ConfigMissing extends Schema.TaggedError()("ConfigMissing", {}) {
|
|
37
26
|
}
|
|
38
27
|
class ConfigFileError extends Schema.TaggedError()("ConfigFileError", {
|
|
@@ -41,12 +30,17 @@ class ConfigFileError extends Schema.TaggedError()("ConfigFileError", {
|
|
|
41
30
|
}
|
|
42
31
|
const fileApiKey = (provider) => Config.redacted("apiKey").pipe(Config.nested(provider), Config.nested("providers"));
|
|
43
32
|
const RuntimeConfig = Config.all({
|
|
44
|
-
provider: Config.schema(
|
|
33
|
+
provider: Config.schema(ProviderPreference, "provider"),
|
|
45
34
|
zen: Config.option(fileApiKey("zen").pipe(Config.orElse(() => Config.redacted("OPENCODE_API_KEY")))),
|
|
46
35
|
typesafe: Config.option(fileApiKey("typesafe").pipe(Config.orElse(() => Config.redacted("TYPESAFE_API_KEY")))),
|
|
47
|
-
|
|
36
|
+
openrouter: Config.option(fileApiKey("openrouter").pipe(Config.orElse(() => Config.redacted("OPENROUTER_API_KEY")))),
|
|
37
|
+
vercel: Config.option(fileApiKey("vercel").pipe(Config.orElse(() => Config.redacted("AI_GATEWAY_API_KEY")))),
|
|
38
|
+
questions: Config.option(Config.schema(ApprovalQuestions, ["permissions", "questions"])),
|
|
48
39
|
});
|
|
49
|
-
const hasCredentials = (document) => document.providers?.zen !== undefined ||
|
|
40
|
+
const hasCredentials = (document) => document.providers?.zen !== undefined ||
|
|
41
|
+
document.providers?.typesafe !== undefined ||
|
|
42
|
+
document.providers?.openrouter !== undefined ||
|
|
43
|
+
document.providers?.vercel !== undefined;
|
|
50
44
|
const readDocument = Effect.fn("JevvyConfig.readDocument")(function* (path) {
|
|
51
45
|
const raw = yield* Effect.tryPromise({
|
|
52
46
|
try: () => readFile(path, "utf8"),
|
|
@@ -101,11 +95,12 @@ const load = Effect.fn("JevvyConfig.load")(function* (path, environment) {
|
|
|
101
95
|
apiKeys = { zen: config.zen.value };
|
|
102
96
|
if (Option.isSome(config.typesafe))
|
|
103
97
|
apiKeys = { ...apiKeys, typesafe: config.typesafe.value };
|
|
98
|
+
if (Option.isSome(config.openrouter))
|
|
99
|
+
apiKeys = { ...apiKeys, openrouter: config.openrouter.value };
|
|
100
|
+
if (Option.isSome(config.vercel))
|
|
101
|
+
apiKeys = { ...apiKeys, vercel: config.vercel.value };
|
|
104
102
|
if (Option.isNone(config.questions))
|
|
105
103
|
return { kind: "default", provider: config.provider, apiKeys };
|
|
106
|
-
if (Object.keys(config.questions.value).length === 0) {
|
|
107
|
-
return { kind: "invalid", message: "permissions.questions must contain at least one question" };
|
|
108
|
-
}
|
|
109
104
|
return { kind: "custom", provider: config.provider, apiKeys, questions: config.questions.value };
|
|
110
105
|
}, Effect.catchTag("ConfigFileError", (error) => Effect.succeed({ kind: "invalid", message: invalidMessage(error.reason) })));
|
|
111
106
|
export const loadJevvyConfig = (path = globalJevvyConfigPath(), environment = ConfigProvider.fromEnv()) => load(path, environment);
|
package/dist/core.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createTypeSafeClient, createZenClient, DEFAULT_TYPESAFE_MODEL, DEFAULT_ZEN_MODEL, } from "@jevvy/core";
|
|
2
|
-
export type {
|
|
1
|
+
export { createOpenRouterClient, createTypeSafeClient, createVercelClient, createZenClient, DEFAULT_OPENROUTER_MODEL, DEFAULT_TYPESAFE_MODEL, DEFAULT_VERCEL_MODEL, DEFAULT_ZEN_MODEL, isJevProviderError, JevRequest, JevResult, JevProvider, JevProviderError, JevProviderFailure, Json, NoulAnswer, NoulQuestion, providerFailureOf, } from "@jevvy/core";
|
|
2
|
+
export type { JevClient, JevProviderErrorKind, } from "@jevvy/core";
|