@mhingston5/jev-cli 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Hingston
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Jev CLI
2
+
3
+ A small, provider-agnostic command-line interface and Node.js client for [TypeSafe AI Jev](https://typesafe.ai/).
4
+
5
+ The package owns the reusable Jev boundary: provider selection, System One transport, typed question construction, and the `jev` executable. Domain-specific tools such as `jev-agent-browser` can depend on this package while keeping their own observation, policy, and execution loops.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @mhingston5/jev-cli
11
+ ```
12
+
13
+ Node.js 20 or newer is required.
14
+
15
+ ```bash
16
+ export TYPESAFE_API_KEY="..."
17
+ jev noul \
18
+ --state "Please refund this today" \
19
+ --question "Does the message communicate urgency?"
20
+ ```
21
+
22
+ The CLI never accepts API keys as command-line arguments. Supply credentials through environment variables or your secret manager.
23
+
24
+ ## Commands
25
+
26
+ ### Noul
27
+
28
+ ```bash
29
+ jev noul \
30
+ --state '{"ticket":"Please fix this ASAP"}' \
31
+ --question "Does this communicate urgency?" \
32
+ --answer-only
33
+ ```
34
+
35
+ ### Choice
36
+
37
+ ```bash
38
+ jev choice \
39
+ --state '{"ticket":"I was charged twice"}' \
40
+ --question "Which team should handle this?" \
41
+ --choices '{"billing":"Payments and refunds","technical":"Bugs and outages","other":"Anything else"}'
42
+ ```
43
+
44
+ ### Score
45
+
46
+ ```bash
47
+ jev score \
48
+ --state "The service is unusable and blocking production" \
49
+ --question "How severe is this?" \
50
+ --levels '["minor","degraded","blocking"]'
51
+ ```
52
+
53
+ ### Batch
54
+
55
+ ```bash
56
+ cat ticket.json | jev run --questions '{
57
+ "urgent": {
58
+ "type": "noul",
59
+ "instructions": "Does the ticket communicate urgency?"
60
+ },
61
+ "queue": {
62
+ "type": "choice",
63
+ "instructions": "Which team should handle this?",
64
+ "choices": {
65
+ "billing": "Payments and refunds",
66
+ "technical": "Bugs and outages"
67
+ }
68
+ },
69
+ "severity": {
70
+ "type": "score",
71
+ "instructions": "How severe is the impact?",
72
+ "levels": ["minor", "degraded", "blocking"]
73
+ }
74
+ }'
75
+ ```
76
+
77
+ Use `--questions-file questions.json` for checked-in question definitions. State can be supplied using `--state`, `--state-file`, or stdin.
78
+
79
+ ## Providers
80
+
81
+ | Provider | CLI value | Credentials | Default model |
82
+ | --- | --- | --- | --- |
83
+ | TypeSafe | `typesafe` | `TYPESAFE_API_KEY` | `jev-1.13.0` |
84
+ | Vercel AI Gateway | `vercel` | `AI_GATEWAY_API_KEY` | `typesafe-ai/jev` |
85
+ | Cloudflare AI | `cloudflare` | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` | `typesafe/jev` |
86
+ | Custom | `custom` | `JEV_API_KEY` when required | caller-defined / Jev default |
87
+
88
+ Select the transport with `--provider` or `JEV_PROVIDER`. Override the model with `--model` or `JEV_MODEL`, and the endpoint with `--endpoint` or `JEV_ENDPOINT`.
89
+
90
+ ```bash
91
+ jev doctor --provider vercel
92
+ ```
93
+
94
+ `doctor` reports provider/model configuration and whether expected credential environment variables are present. It never prints credential values.
95
+
96
+ ## Library API
97
+
98
+ ```ts
99
+ import { createJevClient, choice, noul, score } from "@mhingston5/jev-cli";
100
+
101
+ const client = createJevClient({ provider: "typesafe" });
102
+
103
+ const response = await client.systemOne({
104
+ state: { ticket: "I was charged twice and need one charge refunded today." },
105
+ questions: {
106
+ intent: choice("What is the customer's main request?", {
107
+ refund: "The customer wants money returned.",
108
+ technical: "The customer needs technical help.",
109
+ }),
110
+ urgent: noul("Does the ticket explicitly communicate time pressure?"),
111
+ severity: score("How severe is the impact?", ["minor", "degraded", "blocking"]),
112
+ },
113
+ });
114
+
115
+ console.log(response.answers);
116
+ ```
117
+
118
+ For dynamically constructed question definitions, `buildQuestion()` and `buildQuestions()` accept JSON-friendly specs. The package also exports `SystemOneLikeClient` so higher-level libraries can inject deterministic fixture clients in tests.
119
+
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ npm install
125
+ npm run typecheck
126
+ npm test
127
+ npm run build
128
+ npm run pack:check
129
+ ```
130
+
131
+ ## Publishing
132
+
133
+ The `publish` workflow publishes `@mhingston5/jev-cli` on a `v*` tag or manual workflow dispatch. The first npm publication requires an `NPM_TOKEN` repository secret with permission to publish the package. After the package exists, the workflow can move to npm trusted publishing.
134
+
135
+ ## License
136
+
137
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { JEV_PROVIDERS, createJevClient, credentialEnvironmentForProvider, defaultModelForProvider, resolveJevProvider, } from "./client.js";
4
+ import { buildQuestions } from "./questions.js";
5
+ function usage(exitCode = 0) {
6
+ const text = `Usage:
7
+ jev noul --question <text> [state options] [provider options]
8
+ jev choice --question <text> --choices <json> [state options] [provider options]
9
+ jev score --question <text> --levels <json> [state options] [provider options]
10
+ jev run (--questions <json> | --questions-file <path>) [state options] [provider options]
11
+ jev doctor [provider options]
12
+
13
+ State options:
14
+ --state <text|json> State inline. JSON is parsed when valid.
15
+ --state-file <path> Read state from a file. JSON is parsed when valid.
16
+ If omitted, state is read from stdin.
17
+
18
+ Question options:
19
+ --true-label <text> Optional Noul true criterion.
20
+ --false-label <text> Optional Noul false criterion.
21
+ --choices <json> Choice map.
22
+ --levels <json> Ordered Score levels.
23
+ --questions <json> Batch question specs keyed by output id.
24
+ --questions-file <path> Read batch question specs from JSON.
25
+
26
+ Provider options:
27
+ --provider <typesafe|vercel|cloudflare|custom>
28
+ --model <id>
29
+ --endpoint <url>
30
+ --account-id <id> Cloudflare account id; env is preferred.
31
+
32
+ Output options:
33
+ --answer-only Print only the answer for single-question commands.
34
+ --compact Emit compact JSON.
35
+
36
+ Credentials are read from environment variables, not command-line flags.`;
37
+ (exitCode === 0 ? console.log : console.error)(text);
38
+ process.exit(exitCode);
39
+ }
40
+ function parseFlags(args) {
41
+ const flags = new Map();
42
+ for (let index = 0; index < args.length; index += 1) {
43
+ const token = args[index];
44
+ if (!token.startsWith("--"))
45
+ throw new Error(`unexpected positional argument: ${token}`);
46
+ const next = args[index + 1];
47
+ if (next != null && !next.startsWith("--")) {
48
+ const values = flags.get(token) ?? [];
49
+ values.push(next);
50
+ flags.set(token, values);
51
+ index += 1;
52
+ }
53
+ else {
54
+ flags.set(token, []);
55
+ }
56
+ }
57
+ return flags;
58
+ }
59
+ function hasFlag(flags, name) {
60
+ return flags.has(name);
61
+ }
62
+ function flagValue(flags, name) {
63
+ const values = flags.get(name);
64
+ return values?.[values.length - 1];
65
+ }
66
+ function requiredFlag(flags, name) {
67
+ const value = flagValue(flags, name);
68
+ if (value == null)
69
+ throw new Error(`${name} is required`);
70
+ return value;
71
+ }
72
+ function parseMaybeJson(text) {
73
+ const trimmed = text.trim();
74
+ if (!trimmed)
75
+ return "";
76
+ try {
77
+ return JSON.parse(trimmed);
78
+ }
79
+ catch {
80
+ return text;
81
+ }
82
+ }
83
+ async function readStdin() {
84
+ let input = "";
85
+ for await (const chunk of process.stdin)
86
+ input += chunk;
87
+ return input;
88
+ }
89
+ async function readState(flags) {
90
+ const inline = flagValue(flags, "--state");
91
+ const file = flagValue(flags, "--state-file");
92
+ if (inline != null && file != null)
93
+ throw new Error("use --state or --state-file, not both");
94
+ if (inline != null)
95
+ return parseMaybeJson(inline);
96
+ if (file != null)
97
+ return parseMaybeJson(await readFile(file, "utf8"));
98
+ if (process.stdin.isTTY)
99
+ throw new Error("state is required via --state, --state-file, or stdin");
100
+ const stdin = await readStdin();
101
+ if (!stdin.trim())
102
+ throw new Error("stdin did not contain state");
103
+ return parseMaybeJson(stdin);
104
+ }
105
+ function parseJson(text, label) {
106
+ try {
107
+ return JSON.parse(text);
108
+ }
109
+ catch {
110
+ throw new Error(`${label} must be valid JSON`);
111
+ }
112
+ }
113
+ async function readQuestions(flags) {
114
+ const inline = flagValue(flags, "--questions");
115
+ const file = flagValue(flags, "--questions-file");
116
+ if ((inline == null) === (file == null)) {
117
+ throw new Error("provide exactly one of --questions or --questions-file");
118
+ }
119
+ const text = inline ?? await readFile(file, "utf8");
120
+ const value = parseJson(text, "questions");
121
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
122
+ throw new Error("questions must be a JSON object keyed by question id");
123
+ }
124
+ return value;
125
+ }
126
+ function clientOptions(flags) {
127
+ const providerText = flagValue(flags, "--provider");
128
+ if (providerText != null && !JEV_PROVIDERS.includes(providerText)) {
129
+ throw new Error(`--provider must be one of ${JEV_PROVIDERS.join(", ")}`);
130
+ }
131
+ return {
132
+ provider: providerText,
133
+ model: flagValue(flags, "--model"),
134
+ endpoint: flagValue(flags, "--endpoint"),
135
+ accountId: flagValue(flags, "--account-id"),
136
+ };
137
+ }
138
+ function output(value, compact) {
139
+ console.log(JSON.stringify(value, null, compact ? 0 : 2));
140
+ }
141
+ async function runQuestion(state, specs, flags, answerOnly) {
142
+ const options = clientOptions(flags);
143
+ const provider = resolveJevProvider(options);
144
+ const model = options.model ?? defaultModelForProvider(provider);
145
+ const client = createJevClient(options);
146
+ const response = await client.systemOne({ model, state, questions: buildQuestions(specs) });
147
+ if (answerOnly && Object.keys(specs).length === 1) {
148
+ const key = Object.keys(specs)[0];
149
+ output(response.answers[key], hasFlag(flags, "--compact"));
150
+ return;
151
+ }
152
+ output(response, hasFlag(flags, "--compact"));
153
+ }
154
+ async function main() {
155
+ const [command, ...rest] = process.argv.slice(2);
156
+ if (!command || command === "--help" || command === "-h" || command === "help")
157
+ usage(0);
158
+ const flags = parseFlags(rest);
159
+ if (command === "doctor") {
160
+ const options = clientOptions(flags);
161
+ const provider = resolveJevProvider(options);
162
+ const credentialEnvironment = credentialEnvironmentForProvider(provider);
163
+ output({
164
+ provider,
165
+ model: options.model ?? defaultModelForProvider(provider),
166
+ credentials: Object.fromEntries(credentialEnvironment.map((name) => [name, Boolean(process.env[name])])),
167
+ endpointOverride: Boolean(options.endpoint ?? process.env.JEV_ENDPOINT),
168
+ }, hasFlag(flags, "--compact"));
169
+ return;
170
+ }
171
+ const state = await readState(flags);
172
+ if (command === "noul") {
173
+ await runQuestion(state, {
174
+ result: {
175
+ type: "noul",
176
+ instructions: requiredFlag(flags, "--question"),
177
+ ...(flagValue(flags, "--true-label") != null || flagValue(flags, "--false-label") != null
178
+ ? { labels: { true: flagValue(flags, "--true-label"), false: flagValue(flags, "--false-label") } }
179
+ : {}),
180
+ },
181
+ }, flags, hasFlag(flags, "--answer-only"));
182
+ return;
183
+ }
184
+ if (command === "choice") {
185
+ const choices = parseJson(requiredFlag(flags, "--choices"), "--choices");
186
+ if (!choices || typeof choices !== "object" || Array.isArray(choices)) {
187
+ throw new Error("--choices must be a JSON object");
188
+ }
189
+ await runQuestion(state, {
190
+ result: {
191
+ type: "choice",
192
+ instructions: requiredFlag(flags, "--question"),
193
+ choices: choices,
194
+ },
195
+ }, flags, hasFlag(flags, "--answer-only"));
196
+ return;
197
+ }
198
+ if (command === "score") {
199
+ const levels = parseJson(requiredFlag(flags, "--levels"), "--levels");
200
+ if (!Array.isArray(levels) || levels.some((item) => typeof item !== "string")) {
201
+ throw new Error("--levels must be a JSON array of strings");
202
+ }
203
+ await runQuestion(state, {
204
+ result: {
205
+ type: "score",
206
+ instructions: requiredFlag(flags, "--question"),
207
+ levels: levels,
208
+ },
209
+ }, flags, hasFlag(flags, "--answer-only"));
210
+ return;
211
+ }
212
+ if (command === "run") {
213
+ await runQuestion(state, await readQuestions(flags), flags, false);
214
+ return;
215
+ }
216
+ throw new Error(`unknown command: ${command}`);
217
+ }
218
+ main().catch((error) => {
219
+ const message = error instanceof Error ? error.message : String(error);
220
+ console.error(`jev: ${message}`);
221
+ process.exitCode = 1;
222
+ });
package/dist/client.js ADDED
@@ -0,0 +1,210 @@
1
+ import { TypeSafeClient } from "@typesafe-ai/sdk";
2
+ export const JEV_PROVIDERS = ["typesafe", "vercel", "cloudflare", "custom"];
3
+ export const DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
4
+ export const VERCEL_ENDPOINT = "https://ai-gateway.vercel.sh/typesafe/v1/systemone";
5
+ function endpointRoot(endpoint) {
6
+ const url = new URL(endpoint);
7
+ return `${url.origin}${url.pathname.replace(/\/v1\/systemone\/?$/, "")}`.replace(/\/$/, "");
8
+ }
9
+ function responsePayload(payload) {
10
+ const body = payload?.data ?? payload;
11
+ const value = body?.answers ? body : body?.result?.answers ? body.result : body?.output?.answers ? body.output : body;
12
+ if (!value || typeof value !== "object" || !value.answers || typeof value.answers !== "object") {
13
+ throw new Error("Jev API response did not contain answers");
14
+ }
15
+ return {
16
+ model: typeof value.model === "string" ? value.model : "jev",
17
+ answers: value.answers,
18
+ usage: value.usage,
19
+ };
20
+ }
21
+ function errorMessage(body) {
22
+ return body?.error?.message ?? body?.errors?.[0]?.message ?? body?.message ?? "request failed";
23
+ }
24
+ export function resolveJevProvider(options = {}) {
25
+ if (options.provider)
26
+ return options.provider;
27
+ const env = process.env.JEV_PROVIDER?.trim().toLowerCase();
28
+ if (env && JEV_PROVIDERS.includes(env))
29
+ return env;
30
+ return "typesafe";
31
+ }
32
+ export function defaultModelForProvider(provider) {
33
+ const override = process.env.JEV_MODEL?.trim();
34
+ if (override)
35
+ return override;
36
+ switch (provider) {
37
+ case "typesafe":
38
+ return process.env.TYPESAFE_DEFAULT_MODEL?.trim() || "jev-1.13.0";
39
+ case "vercel":
40
+ return "typesafe-ai/jev";
41
+ case "cloudflare":
42
+ return "typesafe/jev";
43
+ case "custom":
44
+ return process.env.TYPESAFE_DEFAULT_MODEL?.trim() || "jev-1.13.0";
45
+ }
46
+ }
47
+ export function credentialEnvironmentForProvider(provider) {
48
+ switch (provider) {
49
+ case "typesafe": return ["TYPESAFE_API_KEY"];
50
+ case "vercel": return ["AI_GATEWAY_API_KEY"];
51
+ case "cloudflare": return ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"];
52
+ case "custom": return ["JEV_API_KEY"];
53
+ }
54
+ }
55
+ function defaultApiKey(provider) {
56
+ switch (provider) {
57
+ case "typesafe": return process.env.TYPESAFE_API_KEY;
58
+ case "vercel": return process.env.AI_GATEWAY_API_KEY;
59
+ case "cloudflare": return process.env.CLOUDFLARE_API_TOKEN;
60
+ case "custom": return process.env.JEV_API_KEY ?? process.env.TYPESAFE_API_KEY;
61
+ }
62
+ }
63
+ export function defaultEndpointForProvider(provider, accountId) {
64
+ const override = process.env.JEV_ENDPOINT?.trim();
65
+ if (override)
66
+ return override;
67
+ switch (provider) {
68
+ case "typesafe":
69
+ return process.env.TYPESAFE_BASE_URL?.trim() || DEFAULT_ENDPOINT;
70
+ case "vercel":
71
+ return VERCEL_ENDPOINT;
72
+ case "cloudflare": {
73
+ const id = accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID?.trim();
74
+ if (!id)
75
+ throw new Error("CLOUDFLARE_ACCOUNT_ID is required for the cloudflare provider unless --endpoint is supplied");
76
+ return `https://api.cloudflare.com/client/v4/accounts/${id}/ai/run`;
77
+ }
78
+ case "custom":
79
+ return DEFAULT_ENDPOINT;
80
+ }
81
+ }
82
+ class TypeSafeCompatibleJevClient {
83
+ client;
84
+ model;
85
+ constructor(provider, options = {}) {
86
+ const apiKey = options.apiKey ?? defaultApiKey(provider);
87
+ if (!apiKey) {
88
+ throw new Error(provider === "vercel"
89
+ ? "AI_GATEWAY_API_KEY is required for the vercel provider"
90
+ : "TYPESAFE_API_KEY is required for the typesafe provider");
91
+ }
92
+ const endpoint = options.endpoint ?? defaultEndpointForProvider(provider, options.accountId);
93
+ this.model = options.model ?? defaultModelForProvider(provider);
94
+ this.client = new TypeSafeClient({
95
+ apiKey,
96
+ baseURL: endpointRoot(endpoint),
97
+ defaultModel: this.model,
98
+ timeout: options.timeoutMs,
99
+ defaultHeaders: options.headers,
100
+ ...(options.fetchImpl ? { fetch: options.fetchImpl } : {}),
101
+ });
102
+ }
103
+ async systemOne(request) {
104
+ return this.client.systemOne({ ...request, model: this.model });
105
+ }
106
+ }
107
+ export class FetchJevClient {
108
+ endpoint;
109
+ apiKey;
110
+ model;
111
+ timeoutMs;
112
+ headers;
113
+ fetchImpl;
114
+ constructor(options = {}) {
115
+ this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
116
+ this.apiKey = options.apiKey ?? defaultApiKey("custom");
117
+ this.model = options.model;
118
+ this.timeoutMs = options.timeoutMs ?? 30_000;
119
+ this.headers = { "content-type": "application/json", ...(options.headers ?? {}) };
120
+ this.fetchImpl = options.fetchImpl ?? fetch;
121
+ }
122
+ async systemOne(request) {
123
+ const controller = new AbortController();
124
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
125
+ try {
126
+ const response = await this.fetchImpl(this.endpoint, {
127
+ method: "POST",
128
+ headers: {
129
+ ...this.headers,
130
+ ...(this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}),
131
+ },
132
+ body: JSON.stringify({ ...request, ...(this.model ? { model: this.model } : {}) }),
133
+ signal: controller.signal,
134
+ });
135
+ const body = await response.json().catch(() => undefined);
136
+ if (!response.ok)
137
+ throw new Error(`Jev API ${response.status}: ${errorMessage(body)}`);
138
+ return responsePayload(body);
139
+ }
140
+ catch (error) {
141
+ if (error instanceof Error && error.name === "AbortError") {
142
+ throw new Error(`Jev API timed out after ${this.timeoutMs}ms`);
143
+ }
144
+ throw error;
145
+ }
146
+ finally {
147
+ clearTimeout(timer);
148
+ }
149
+ }
150
+ }
151
+ export class CloudflareJevClient {
152
+ endpoint;
153
+ apiKey;
154
+ model;
155
+ timeoutMs;
156
+ headers;
157
+ fetchImpl;
158
+ constructor(options = {}) {
159
+ this.endpoint = options.endpoint ?? defaultEndpointForProvider("cloudflare", options.accountId);
160
+ const apiKey = options.apiKey ?? defaultApiKey("cloudflare");
161
+ if (!apiKey)
162
+ throw new Error("CLOUDFLARE_API_TOKEN is required for the cloudflare provider");
163
+ this.apiKey = apiKey;
164
+ this.model = options.model ?? defaultModelForProvider("cloudflare");
165
+ this.timeoutMs = options.timeoutMs ?? 30_000;
166
+ this.headers = { "content-type": "application/json", ...(options.headers ?? {}) };
167
+ this.fetchImpl = options.fetchImpl ?? fetch;
168
+ }
169
+ async systemOne(request) {
170
+ const controller = new AbortController();
171
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
172
+ try {
173
+ const response = await this.fetchImpl(this.endpoint, {
174
+ method: "POST",
175
+ headers: { ...this.headers, authorization: `Bearer ${this.apiKey}` },
176
+ body: JSON.stringify({
177
+ model: this.model,
178
+ input: { state: request.state, questions: request.questions },
179
+ }),
180
+ signal: controller.signal,
181
+ });
182
+ const body = await response.json().catch(() => undefined);
183
+ if (!response.ok)
184
+ throw new Error(`Jev API ${response.status}: ${errorMessage(body)}`);
185
+ return responsePayload(body);
186
+ }
187
+ catch (error) {
188
+ if (error instanceof Error && error.name === "AbortError") {
189
+ throw new Error(`Jev API timed out after ${this.timeoutMs}ms`);
190
+ }
191
+ throw error;
192
+ }
193
+ finally {
194
+ clearTimeout(timer);
195
+ }
196
+ }
197
+ }
198
+ export function createJevClient(options = {}) {
199
+ const provider = resolveJevProvider(options);
200
+ if (provider === "cloudflare")
201
+ return new CloudflareJevClient(options);
202
+ if (provider === "custom") {
203
+ return new FetchJevClient({
204
+ ...options,
205
+ endpoint: options.endpoint ?? process.env.JEV_ENDPOINT ?? DEFAULT_ENDPOINT,
206
+ model: options.model ?? process.env.JEV_MODEL,
207
+ });
208
+ }
209
+ return new TypeSafeCompatibleJevClient(provider, options);
210
+ }
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export * from "./client.js";
3
+ export * from "./questions.js";
4
+ export { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";
@@ -0,0 +1,45 @@
1
+ import { choice, noul, score } from "@typesafe-ai/sdk";
2
+ function requireInstructions(instructions) {
3
+ if (!instructions.trim())
4
+ throw new Error("question instructions must not be empty");
5
+ }
6
+ export function buildQuestion(spec) {
7
+ requireInstructions(spec.instructions);
8
+ if (spec.type === "noul") {
9
+ if (!spec.labels)
10
+ return noul(spec.instructions);
11
+ return noul(spec.instructions, {
12
+ true: spec.labels.true ?? "The condition is true.",
13
+ false: spec.labels.false ?? "The condition is false.",
14
+ });
15
+ }
16
+ if (spec.type === "choice") {
17
+ const entries = Object.entries(spec.choices);
18
+ if (entries.length < 2)
19
+ throw new Error("choice questions require at least two choices");
20
+ if (entries.length > 255)
21
+ throw new Error("choice questions support at most 255 choices");
22
+ if (entries.some(([id]) => !id.trim()))
23
+ throw new Error("choice ids must not be empty");
24
+ return choice(spec.instructions, spec.choices);
25
+ }
26
+ if (spec.levels.length < 2)
27
+ throw new Error("score questions require at least two levels");
28
+ if (spec.levels.length > 10)
29
+ throw new Error("score questions support at most ten levels");
30
+ if (spec.levels.some((level) => !level.trim()))
31
+ throw new Error("score levels must not be empty");
32
+ return score(spec.instructions, spec.levels);
33
+ }
34
+ export function buildQuestions(specs) {
35
+ const entries = Object.entries(specs);
36
+ if (!entries.length)
37
+ throw new Error("at least one question is required");
38
+ const result = {};
39
+ for (const [id, spec] of entries) {
40
+ if (!id.trim())
41
+ throw new Error("question ids must not be empty");
42
+ result[id] = buildQuestion(spec);
43
+ }
44
+ return result;
45
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@mhingston5/jev-cli",
3
+ "version": "0.1.0",
4
+ "description": "Provider-agnostic CLI and client for TypeSafe AI Jev",
5
+ "license": "MIT",
6
+ "author": "Mark Hingston",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mhingston/jev-cli.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/mhingston/jev-cli/issues"
13
+ },
14
+ "homepage": "https://github.com/mhingston/jev-cli#readme",
15
+ "keywords": [
16
+ "jev",
17
+ "typesafe",
18
+ "system-one",
19
+ "typed-decisions",
20
+ "cli"
21
+ ],
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "type": "module",
34
+ "main": "dist/index.js",
35
+ "types": "dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js"
40
+ }
41
+ },
42
+ "bin": {
43
+ "jev": "dist/cli.js"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.json",
47
+ "prepublishOnly": "npm run typecheck && npm test && npm run build",
48
+ "pack:check": "npm pack --dry-run",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest"
52
+ },
53
+ "dependencies": {
54
+ "@typesafe-ai/sdk": "^0.6.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^22.0.0",
58
+ "typescript": "^5.7.0",
59
+ "vitest": "^5.0.1"
60
+ }
61
+ }