jevable 0.1.1 → 0.1.2

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
@@ -4,7 +4,9 @@ Make your monitor smart. An agent watching a log, a feed or an API is woken
4
4
  by every line, or by a grep that misses what nobody foresaw. `jevable` sits in
5
5
  between: plain conditions do what grep does, and a question answered by
6
6
  [Jev](https://docs.typesafe.ai), TypeSafe's fast and cheap classification
7
- model, decides the rest. Only what matters wakes the agent, or reaches you.
7
+ model, decides the rest. Only what matters wakes the agent, or reaches you —
8
+ and every wake it holds back is an agent turn you do not pay for, while a Jev
9
+ question costs about $0.00002.
8
10
 
9
11
  ## One prompt, any agent
10
12
 
@@ -26,13 +28,14 @@ The agent writes the rule, tests it on real events and on the edge cases you
26
28
  named, and arms it the way its runtime can be woken: a background command
27
29
  that ends at the next match and wakes the agent (restarted after each one), a
28
30
  stream of notifications (Claude Code's Monitor), or a watch that runs on after
29
- the session and pushes to your phone or resumes the session. It asks you once
30
- for a TypeSafe API key and keeps it in `~/.jevable/key`.
31
+ the session and pushes to your phone or resumes the session. For Jev it needs
32
+ one API key from TypeSafe, OpenRouter or Vercel AI Gateway: it finds the one
33
+ you have, or asks you to paste one and sets it up.
31
34
 
32
35
  ## By hand
33
36
 
34
37
  ```bash
35
- export TYPESAFE_API_KEY=... # or put it in ~/.jevable/key
38
+ npx -y jevable key sk-or-v1-... # an API key from TypeSafe, OpenRouter or Vercel AI Gateway
36
39
  npx -y jevable filter --json --from 'tail -n 0 -F app.log' \
37
40
  'line.contains("ERROR") && judge.boolean(line, "Does this log line report that a service or a dependency it needs is down or unreachable?") >= 0.7'
38
41
  ```
@@ -41,6 +44,7 @@ Node 20+. `npx -y jevable <command>` needs no install; `npm i -g jevable` gives
41
44
 
42
45
  - `jevable filter [RULE]` — print what passes, from stdin or `--from CMD`. `--json`, `--key`, `--cooldown`, `--window`, `-m`, `--state`, `--all`.
43
46
  - `jevable test [RULE] --yes ... --no ...` — run a rule on samples, show the scores and the thresholds that separate them.
47
+ - `jevable key [KEY]` — whether there is a working key for Jev; given an API key from TypeSafe, OpenRouter or Vercel AI Gateway, check it and save it.
44
48
  - `jevable guide` — everything an agent needs: the steps, how each runtime gets the events back, rules, options, recipes ([guide.md](guide.md)).
45
49
 
46
50
  ## As a library
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // The jevable command line: filter, test, guide.
3
3
  import { filterCommand } from "./commands/filter.js";
4
+ import { keyCommand } from "./commands/key.js";
4
5
  import { testCommand } from "./commands/test.js";
5
6
  import { log, packageFile } from "./common.js";
6
7
  const HELP = `jevable — make your monitor smart
@@ -15,11 +16,11 @@ a fast and cheap classification model, a semantic question.
15
16
  Commands:
16
17
  filter [RULE] print the records (or windows) that pass RULE
17
18
  test [RULE] --yes .. --no .. run RULE on samples and show the scores
19
+ key [KEY] whether there is a working key for Jev; with KEY, check and save it
18
20
  guide what to do and how: steps for agents, rules, options, recipes
19
21
 
20
22
  Agents: read \`jevable guide\` first and follow it.
21
- Setup: export TYPESAFE_API_KEY (or JEV_API_KEY), or put the key in ~/.jevable/key.
22
- JEV_BASE_URL for a proxy, JEV_MODEL to pin a model.
23
+ Setup: \`jevable key\`. Any API key from TypeSafe, OpenRouter or Vercel AI Gateway works.
23
24
  Run \`jevable <command> --help\` for a command's options.
24
25
  `;
25
26
  async function main([cmd, ...args]) {
@@ -28,6 +29,8 @@ async function main([cmd, ...args]) {
28
29
  return filterCommand(args);
29
30
  case "test":
30
31
  return testCommand(args);
32
+ case "key":
33
+ return keyCommand(args);
31
34
  case "guide":
32
35
  process.stdout.write(packageFile("guide.md"));
33
36
  return 0;
@@ -0,0 +1,2 @@
1
+ export declare const KEY_HELP = "jevable key [KEY]\n\nWithout KEY: whether jevable has a key for Jev, and whether it works.\nWith KEY: tell whose key it is (TypeSafe, OpenRouter or Vercel AI Gateway),\ncheck it with one question, and save it in ~/.jevable/env, where every\nruntime and detached watch finds it.\n\nExit status: 0 when a working key is in place, 1 when not.\n";
2
+ export declare function keyCommand(args: string[]): Promise<number>;
@@ -0,0 +1,77 @@
1
+ // `jevable key`: is there a key for Jev, and does it work? Given a key: whose
2
+ // it is, checked with one question and saved where every runtime and detached
3
+ // watch finds it. The person pastes a key; they never pick a provider or a
4
+ // variable name.
5
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname } from "node:path";
7
+ import { parseArgs } from "node:util";
8
+ import { Client, lookup, whose } from "@jevable/core";
9
+ import { ENV_FILE, provider, settings } from "../common.js";
10
+ export const KEY_HELP = `jevable key [KEY]
11
+
12
+ Without KEY: whether jevable has a key for Jev, and whether it works.
13
+ With KEY: tell whose key it is (TypeSafe, OpenRouter or Vercel AI Gateway),
14
+ check it with one question, and save it in ~/.jevable/env, where every
15
+ runtime and detached watch finds it.
16
+
17
+ Exit status: 0 when a working key is in place, 1 when not.
18
+ `;
19
+ const ASK = "Ask the person for an API key from TypeSafe, OpenRouter or Vercel AI Gateway, then run: jevable key <the key>";
20
+ export async function keyCommand(args) {
21
+ const { values: v, positionals } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean", short: "h" } } });
22
+ if (v.help)
23
+ return say(KEY_HELP.trimEnd(), 0);
24
+ const { vars, fromFile } = settings();
25
+ if (positionals[0])
26
+ return save(positionals[0].trim(), vars);
27
+ const found = provider(vars);
28
+ if (!found?.key)
29
+ return say(`No key for Jev. ${ASK}`, 1);
30
+ const where = fromFile.has(found.variable) ? "in ~/.jevable/env" : `from ${found.variable}`;
31
+ const r = await check(found.provider, found.key);
32
+ if ("error" in r)
33
+ return say(`The ${found.provider.label} key ${where} does not work: ${r.error}. ${ASK}`, 1);
34
+ return say(`Jev via ${found.provider.label}, key ${where}; it works (${r.seconds} s).`, 0);
35
+ }
36
+ async function save(key, vars) {
37
+ // A custom endpoint (JEV_BASE_URL) takes any key; otherwise the key says whose it is.
38
+ const p = vars.JEV_BASE_URL ? lookup(vars)[0].provider : whose(key);
39
+ if (!p)
40
+ return say("That is not a key for Jev: TypeSafe keys start with apikey_, OpenRouter keys with sk-or-, Vercel AI Gateway keys with vck_.", 1);
41
+ const r = await check(p, key);
42
+ if ("error" in r)
43
+ return say(`The ${p.label} key does not work: ${r.error}. Nothing saved.`, 1);
44
+ // Name the provider too, so a key saved now wins over an older one elsewhere.
45
+ writeEnv(p.name === "custom" ? { [p.env[0]]: key } : { [p.env[0]]: key, JEV_PROVIDER: p.name });
46
+ return say(`Saved the ${p.label} key in ~/.jevable/env; it works (${r.seconds} s).`, 0);
47
+ }
48
+ /** One question, to see the key and the endpoint work. */
49
+ async function check(p, key) {
50
+ const client = new Client({ apiKey: key, baseUrl: p.baseUrl, model: p.model, provider: p.label });
51
+ const start = performance.now();
52
+ try {
53
+ await client.ask("jevable key", { q: { type: "noul", instructions: "Is this text a command?" } });
54
+ return { seconds: ((performance.now() - start) / 1000).toFixed(2) };
55
+ }
56
+ catch (err) {
57
+ return { error: err.message.replace(/\.$/, "") };
58
+ }
59
+ }
60
+ /** Sets NAME=value lines in ~/.jevable/env, keeping the rest; readable by the owner only. */
61
+ function writeEnv(set) {
62
+ let lines = [];
63
+ try {
64
+ lines = readFileSync(ENV_FILE, "utf8").split("\n").filter(Boolean);
65
+ }
66
+ catch { }
67
+ lines = lines.filter((l) => !Object.keys(set).includes(l.match(/^\s*(?:export\s+)?([A-Za-z_]\w*)\s*=/)?.[1] ?? ""));
68
+ for (const [name, value] of Object.entries(set))
69
+ lines.push(`${name}=${value}`);
70
+ mkdirSync(dirname(ENV_FILE), { recursive: true, mode: 0o700 });
71
+ writeFileSync(ENV_FILE, `${lines.join("\n")}\n`, { mode: 0o600 });
72
+ chmodSync(ENV_FILE, 0o600);
73
+ }
74
+ function say(text, code) {
75
+ process.stdout.write(`${text}\n`);
76
+ return code;
77
+ }
@@ -0,0 +1,2 @@
1
+ export declare const PROVIDERS_HELP = "jevable providers [--check]\n\nWhere jevable can reach Jev (TypeSafe's classification model), the key it\nfound for each \u2014 in the environment or in ~/.jevable/env \u2014 and the one it\nuses: JEV_PROVIDER if set, else the first with a key.\n\nOptions:\n --check ask each provider with a key one question, to see that the key works\n (a fraction of a cent each)\n\nExit status: 0 when a provider has a key (and, with --check, answered), 1 when none does.\n";
2
+ export declare function providersCommand(args: string[]): Promise<number>;
@@ -0,0 +1,54 @@
1
+ // `jevable providers`: where jevable can reach Jev, which key it found for
2
+ // each, and which one it uses — so an agent knows before asking anyone.
3
+ import { parseArgs } from "node:util";
4
+ import { Client, lookup } from "@jevable/core";
5
+ import { provider, settings } from "../common.js";
6
+ export const PROVIDERS_HELP = `jevable providers [--check]
7
+
8
+ Where jevable can reach Jev (TypeSafe's classification model), the key it
9
+ found for each — in the environment or in ~/.jevable/env — and the one it
10
+ uses: JEV_PROVIDER if set, else the first with a key.
11
+
12
+ Options:
13
+ --check ask each provider with a key one question, to see that the key works
14
+ (a fraction of a cent each)
15
+
16
+ Exit status: 0 when a provider has a key (and, with --check, answered), 1 when none does.
17
+ `;
18
+ export async function providersCommand(args) {
19
+ const { values: v } = parseArgs({ args, options: { check: { type: "boolean" }, help: { type: "boolean", short: "h" } } });
20
+ if (v.help) {
21
+ process.stdout.write(PROVIDERS_HELP);
22
+ return 0;
23
+ }
24
+ const { vars, fromFile } = settings();
25
+ const found = lookup(vars);
26
+ const used = provider(vars);
27
+ const where = (f) => (f.variable ? `${f.variable} (${fromFile.has(f.variable) ? "~/.jevable/env" : "environment"})` : `no ${f.provider.env[0]}`);
28
+ const rows = await Promise.all(found.map(async (f) => [f.provider.name, where(f), f.provider.name === used?.provider.name ? "← used" : "", v.check && f.key ? await check(f) : ""]));
29
+ const width = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
30
+ for (const r of rows)
31
+ process.stdout.write(`${r.map((c, i) => c.padEnd(width[i])).join(" ").trimEnd()}\n`);
32
+ if (!used?.key) {
33
+ process.stdout.write(`\nNo key for Jev. Put one of these in ~/.jevable/env (one NAME=value per line):\n`);
34
+ const named = found.filter((f) => f.provider.keys);
35
+ const pad = Math.max(...named.map((f) => f.provider.env[0].length)) + 4;
36
+ for (const f of named)
37
+ process.stdout.write(` ${`${f.provider.env[0]}=...`.padEnd(pad)} ${f.provider.label}, keys at ${f.provider.keys}\n`);
38
+ return 1;
39
+ }
40
+ return v.check && !rows.find((r) => r[2])?.[3].startsWith("ok") ? 1 : 0;
41
+ }
42
+ /** One question, to see the key and the endpoint work. */
43
+ async function check(f) {
44
+ const p = f.provider;
45
+ const client = new Client({ apiKey: f.key, baseUrl: p.baseUrl, model: p.model, provider: p.label });
46
+ const start = performance.now();
47
+ try {
48
+ await client.ask("jevable providers --check", { q: { type: "noul", instructions: "Is this text a command?" } });
49
+ return `ok ${((performance.now() - start) / 1000).toFixed(2)} s`;
50
+ }
51
+ catch (err) {
52
+ return `failed: ${err.message.slice(0, 120)}`;
53
+ }
54
+ }
package/dist/common.d.ts CHANGED
@@ -1,7 +1,19 @@
1
- import { Engine } from "@jevable/core";
1
+ import { Engine, type Found } from "@jevable/core";
2
2
  import type { FilterResult } from "./run/filter.ts";
3
3
  export declare function packageFile(name: string): string;
4
4
  export declare const log: (msg: string) => void;
5
+ export declare const ENV_FILE: string;
6
+ /**
7
+ * The environment, with ~/.jevable/env (NAME=value lines) filling in what it
8
+ * lacks: for runtimes that keep secrets out of a command's environment (dsh
9
+ * drops every variable named *KEY*) and for watches started elsewhere.
10
+ */
11
+ export declare function settings(): {
12
+ vars: Record<string, string | undefined>;
13
+ fromFile: Set<string>;
14
+ };
15
+ /** The provider of Jev to use, from the environment: JEV_PROVIDER, else the first with a key. */
16
+ export declare function provider(vars?: Record<string, string | undefined>): Found | undefined;
5
17
  /** The shared engine, configured from the environment. A missing key is reported by the first judge call. */
6
18
  export declare function newEngine(model?: string): Engine;
7
19
  /** The rule from the positional argument or -f, exactly one. */
package/dist/common.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { readFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { Client, Engine } from "@jevable/core";
6
+ import { choose, Client, Engine, lookup } from "@jevable/core";
7
7
  // guide.md and package.json sit at the package root, next to src/ and dist/.
8
8
  const PACKAGE_ROOT = new URL("../", import.meta.url);
9
9
  export function packageFile(name) {
@@ -12,21 +12,39 @@ export function packageFile(name) {
12
12
  export const log = (msg) => {
13
13
  process.stderr.write(`jevable: ${msg}\n`);
14
14
  };
15
- /** The shared engine, configured from the environment. A missing key is reported by the first judge call. */
16
- export function newEngine(model) {
17
- const env = process.env;
18
- const apiKey = env.JEV_API_KEY || env.TYPESAFE_API_KEY || keyFile();
19
- return new Engine(new Client({ apiKey, baseUrl: env.JEV_BASE_URL, model: model || env.JEV_MODEL }));
20
- }
21
- // ~/.jevable/key: for runtimes that keep secrets out of a command's environment
22
- // (dsh drops every variable named *KEY*) and for watches started elsewhere.
23
- function keyFile() {
15
+ export const ENV_FILE = join(homedir(), ".jevable", "env");
16
+ /**
17
+ * The environment, with ~/.jevable/env (NAME=value lines) filling in what it
18
+ * lacks: for runtimes that keep secrets out of a command's environment (dsh
19
+ * drops every variable named *KEY*) and for watches started elsewhere.
20
+ */
21
+ export function settings() {
22
+ const vars = { ...process.env };
23
+ const fromFile = new Set();
24
+ let text = "";
24
25
  try {
25
- return readFileSync(join(homedir(), ".jevable", "key"), "utf8").trim() || undefined;
26
+ text = readFileSync(ENV_FILE, "utf8");
26
27
  }
27
- catch {
28
- return undefined;
28
+ catch { }
29
+ for (const line of text.split("\n")) {
30
+ const m = line.match(/^\s*(?:export\s+)?([A-Za-z_]\w*)\s*=\s*(.*?)\s*$/);
31
+ if (!m || vars[m[1]])
32
+ continue;
33
+ vars[m[1]] = m[2].replace(/^(["'])(.*)\1$/, "$2");
34
+ fromFile.add(m[1]);
29
35
  }
36
+ return { vars, fromFile };
37
+ }
38
+ /** The provider of Jev to use, from the environment: JEV_PROVIDER, else the first with a key. */
39
+ export function provider(vars = settings().vars) {
40
+ return choose(lookup(vars), vars.JEV_PROVIDER);
41
+ }
42
+ /** The shared engine, configured from the environment. A missing key is reported by the first judge call. */
43
+ export function newEngine(model) {
44
+ const { vars } = settings();
45
+ const found = provider(vars);
46
+ const p = found?.provider;
47
+ return new Engine(new Client({ apiKey: found?.key, baseUrl: p?.baseUrl, model: model || vars.JEV_MODEL || p?.model, provider: p?.label }));
30
48
  }
31
49
  /** The rule from the positional argument or -f, exactly one. */
32
50
  export function ruleSource(positionals, file) {
@@ -44,5 +62,6 @@ export function printStats(r, noun, engine) {
44
62
  const { calls, cacheHits, tokens } = engine.stats;
45
63
  const unit = r.count === 1 ? noun.replace(/s$/, "") : noun;
46
64
  const held = r.emitted >= 0 && r.emitted !== r.passed ? ` (${r.emitted} emitted, the rest held back by --key/--cooldown)` : "";
47
- log(`${r.count} ${unit} · ${r.passed} passed${held} · ${calls} Jev calls (+${cacheHits} from cache) · ${tokens} tokens`);
65
+ const via = calls ? ` via ${engine.client.provider}` : "";
66
+ log(`${r.count} ${unit} · ${r.passed} passed${held} · ${calls} Jev calls${via} (+${cacheHits} from cache) · ${tokens} tokens`);
48
67
  }
package/guide.md CHANGED
@@ -4,7 +4,8 @@ A watch that wakes you on every line buries you in noise; a grep strict enough
4
4
  to stay quiet misses what nobody foresaw. jevable sits between the events and
5
5
  whatever wakes you: plain conditions do what grep does, and a question that
6
6
  Jev (TypeSafe's classification model) answers in about 0.3 s for a tiny
7
- fraction of a cent decides the rest. Only what matters comes out.
7
+ fraction of a cent decides the rest. Only what matters comes out, so you
8
+ miss less and spend a turn only when something counts.
8
9
 
9
10
  jevable filter --json --from 'tail -n 0 -F app.log' \
10
11
  'line.contains("ERROR") && judge.boolean(line, "Does this log line report that a service or a dependency it needs is down or unreachable?") >= 0.7'
@@ -22,12 +23,12 @@ incident", "when a reviewer asks for a change on PR 12, make it". Do this:
22
23
  1. **Pin it down**: where the events come from, what counts, what happens
23
24
  then (you act, or the person is told), and for how long (one event, while
24
25
  this session is open, or days). Ask only what you cannot work out.
25
- 2. **Key.** jevable reads `TYPESAFE_API_KEY` (or `JEV_API_KEY`), else the file
26
- `~/.jevable/key`. If there is none, ask the person for a TypeSafe API key
27
- (docs.typesafe.ai) and save it:
28
- `mkdir -p ~/.jevable && printf '%s\n' "$KEY" > ~/.jevable/key && chmod 600 ~/.jevable/key`.
29
- The file also reaches runtimes that strip keys from a command's environment
30
- (dsh) and watches started outside your shell. Never put the key in a repository.
26
+ 2. **Key.** Run `jevable key`. If it has no working key, ask the person:
27
+ "Do you have an API key from TypeSafe, OpenRouter or Vercel AI Gateway?
28
+ Paste it here." Then run `jevable key <the key>`: it tells whose key it is,
29
+ checks it and saves it where every runtime and detached watch finds it. Do
30
+ not make the person choose a provider or set a variable, and never put a
31
+ key in a repository.
31
32
  3. **Source**: a command that prints one event per line — `tail -n 0 -F` a
32
33
  log, an API polled in a loop, a feed (Recipes). Keep everything for this
33
34
  watch in `~/.jevable/<name>/`; a longer source goes in `source.sh` there. Run it
@@ -149,7 +150,7 @@ Flags change between versions: check them with your runtime's `--help`.
149
150
  out; an extension that watches processes can wake you instead. After the session:
150
151
  `pi -p --session "$PI_SESSION_FILE" "..." </dev/null`, only while no pi
151
152
  window has the session open.
152
- - **dsh.** It strips `*KEY*` variables: use `~/.jevable/key`. A finished
153
+ - **dsh.** It strips `*KEY*` variables: save the key with `jevable key`, which keeps it in a file. A finished
153
154
  background task does not wake an idle session: start the `-m 1` command with
154
155
  `run_in_background: true`, then call `task_output` with `wait: true` and
155
156
  `timeout_ms: 600000` until it ends (blocking). To be woken: only under `dsh web`, by posting a `session.prompt`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jevable",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "grep that reads meaning: pass through only the records a Jev judgement lets through",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "prepack": "npm run build"
28
28
  },
29
29
  "dependencies": {
30
- "@jevable/core": "0.1.1"
30
+ "@jevable/core": "0.1.2"
31
31
  },
32
32
  "license": "UNLICENSED"
33
33
  }