@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 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": "auto",
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 | Default | Options | Purpose |
59
- | --------------------------- | ----------------------- | ------------------------- | ------------------------------------ |
60
- | `provider` | `auto` | `auto`, `zen`, `typesafe` | Select a provider |
61
- | `providers.zen.apiKey` | none | OpenCode API key | Use Zen without an OpenCode login |
62
- | `providers.typesafe.apiKey` | none | TypeSafe API key | Use TypeSafe AI |
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 | Provider |
67
- | -------: | --------------------------------------------- | -------- |
68
- | 1 | OpenCode login | Zen |
69
- | 2 | Global JSON field `providers.zen.apiKey` | Zen |
70
- | 3 | `OPENCODE_API_KEY` | Zen |
71
- | 4 | Global JSON field `providers.typesafe.apiKey` | TypeSafe |
72
- | 5 | `TYPESAFE_API_KEY` | TypeSafe |
73
-
74
- > [!TIP]
75
- > Join the [TypeSafe AI waitlist](https://typesafe.ai/) for $5 in credit.
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 pinned models 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.
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 runCalibration: (options: CalibrationCliOptions, writeStdout?: (text: string) => void, writeStderr?: (text: string) => void) => Promise<number>;
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>;
@@ -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, Redacted, Schema } from "effect";
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 = async (path, name = path) => {
14
- const raw = await readFile(path, "utf8");
15
- return { name, corpus: parseCalibrationCorpus(JSON.parse(raw), name), hash: hash(raw) };
16
- };
17
- const timestamp = () => new Date().toISOString().replaceAll(":", "-").replace(".", "-");
18
- const sleep = (milliseconds) => new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds));
19
- const selectProvider = (preference, keys) => {
20
- if ((preference === "auto" || preference === "zen") && keys.zen !== undefined) {
21
- const key = Redacted.value(keys.zen);
22
- return { provider: "zen", model: DEFAULT_ZEN_MODEL, key, client: createZenClient(key) };
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
- throw new Error("Zen calibration needs providers.zen.apiKey or OPENCODE_API_KEY; OpenCode login is unavailable to the standalone calibration command");
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
- throw new Error("TypeSafe calibration needs providers.typesafe.apiKey or TYPESAFE_API_KEY");
27
+ return "TypeSafe calibration needs providers.typesafe.apiKey or TYPESAFE_API_KEY";
33
28
  }
34
- throw new Error("calibration needs a global or environment provider credential");
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
- catch (error) {
116
- writeStderr(String(error));
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* Effect.promise(() => runCalibration(options));
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([
@@ -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: "typesafe" | "zen";
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: "typesafe" | "zen";
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, Schema } from "effect";
2
- import type { Redacted } from "effect";
3
- import type { ApprovalQuestions } from "./questions.ts";
4
- declare const ProviderPreferenceSchema: Schema.Literals<readonly ["auto", "zen", "typesafe"]>;
5
- export interface ProviderApiKeys {
6
- readonly zen?: Redacted.Redacted<string>;
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
- const Probability = Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 }));
7
- const CriteriaSchema = Schema.Struct({
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.optional(Schema.String),
27
- provider: Schema.optional(ProviderPreferenceSchema),
28
- providers: Schema.optional(Schema.Struct({
29
- zen: Schema.optional(ProviderCredentialSchema),
30
- typesafe: Schema.optional(ProviderCredentialSchema),
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.optional(Schema.Struct({
33
- questions: Schema.optional(ApprovalQuestionsSchema),
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(ProviderPreferenceSchema, "provider"),
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
- questions: Config.option(Config.schema(ApprovalQuestionsSchema, ["permissions", "questions"])),
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 || document.providers?.typesafe !== 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 { Json, JevClient, JevRequest, JevResult, NoulAnswer, NoulQuestion, } from "@jevvy/core";
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";