jev-layer 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -34
- package/CONTRIBUTING.md +146 -146
- package/LICENSE +21 -21
- package/README.md +123 -126
- package/README.ru.md +138 -117
- package/README.zh-CN.md +137 -117
- package/RELEASE.md +53 -53
- package/SECURITY.md +56 -56
- package/bin/jev.mjs +213 -213
- package/config/codex.mcp.toml +8 -8
- package/config/generic-mcp.json +13 -13
- package/config/hermes.mcp.yaml +9 -9
- package/config/jev.example.json +22 -22
- package/config/omp.mcp.json +14 -14
- package/config/providers.env.example +13 -13
- package/docs/AGENT-IMPLEMENTATION.md +250 -0
- package/docs/COMPATIBILITY.md +15 -15
- package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
- package/docs/PROVIDERS.md +67 -0
- package/docs/SCHEMA-VERSIONING.md +96 -94
- package/docs/architecture.svg +77 -0
- package/docs/jev-layer-banner.svg +38 -0
- package/examples/capabilities.json +39 -39
- package/examples/route-request.json +28 -28
- package/integrations/codex/.codex-plugin/plugin.json +19 -19
- package/integrations/codex/.mcp.json +11 -11
- package/integrations/codex/AGENTS.md +1 -1
- package/integrations/codex/run-mcp.mjs +9 -9
- package/integrations/codex/skills/jev-route/SKILL.md +17 -17
- package/integrations/hermes/__init__.py +150 -51
- package/integrations/hermes/plugin.yaml +10 -5
- package/integrations/hermes/schemas.py +89 -12
- package/integrations/omp/extension.js +105 -105
- package/integrations/template/README.md +10 -10
- package/integrations/template/adapter.mjs +87 -87
- package/package.json +60 -59
- package/scripts/benchmark.mjs +35 -35
- package/scripts/browser-benchmark.mjs +92 -92
- package/scripts/browser-e2e.mjs +117 -117
- package/scripts/capability-e2e.mjs +31 -31
- package/scripts/clean-install-smoke.mjs +162 -162
- package/scripts/codex-mcp-smoke.mjs +119 -119
- package/scripts/context-filter-e2e.mjs +45 -45
- package/scripts/fail-open-smoke.mjs +120 -120
- package/scripts/feature-flags-smoke.mjs +46 -46
- package/scripts/mcp-receipt-smoke.mjs +113 -98
- package/scripts/model-route-report.mjs +15 -0
- package/scripts/openrouter-choice.mjs +59 -59
- package/scripts/replay-eval.mjs +124 -124
- package/scripts/smoke.mjs +34 -34
- package/scripts/supervision-e2e.mjs +138 -109
- package/src/browser.mjs +571 -569
- package/src/cli.mjs +33 -33
- package/src/config.mjs +85 -75
- package/src/context-filter.mjs +56 -56
- package/src/contract.mjs +72 -72
- package/src/discovery.mjs +65 -65
- package/src/hermes-adapter.mjs +123 -0
- package/src/mcp-server.mjs +267 -210
- package/src/model-route-metrics.mjs +89 -0
- package/src/model-routing.mjs +87 -0
- package/src/providers/demo.mjs +52 -52
- package/src/providers/index.mjs +12 -0
- package/src/providers/typesafe.mjs +126 -126
- package/src/receipts.mjs +227 -226
- package/src/registry.mjs +109 -109
- package/src/relevance-filter.mjs +99 -99
- package/src/route.mjs +219 -221
- package/src/shadow-compaction.mjs +170 -0
- package/src/supervision.mjs +268 -244
- package/test/browser.test.mjs +199 -199
- package/test/capability.test.mjs +54 -54
- package/test/context-filter.test.mjs +65 -65
- package/test/hermes-adapter.test.mjs +62 -0
- package/test/model-route-metrics.test.mjs +61 -0
- package/test/model-routing.test.mjs +59 -0
- package/test/openrouter-provider.test.mjs +55 -55
- package/test/provider-transports.test.mjs +205 -0
- package/test/providers.test.mjs +126 -0
- package/test/receipts.test.mjs +71 -71
- package/test/route.test.mjs +74 -74
- package/test/shadow-compaction.test.mjs +67 -0
- package/test/supervision.test.mjs +117 -99
package/bin/jev.mjs
CHANGED
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { configuredProvider, loadConfig, pathExists } from "../src/config.mjs";
|
|
6
|
-
|
|
7
|
-
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
-
const [command = "help", ...argv] = process.argv.slice(2);
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
if (command === "mcp") {
|
|
12
|
-
await import("../src/mcp-server.mjs");
|
|
13
|
-
} else if (command === "cli") {
|
|
14
|
-
await import("../src/cli.mjs");
|
|
15
|
-
} else if (command === "install") {
|
|
16
|
-
await install(argv);
|
|
17
|
-
} else if (command === "init") {
|
|
18
|
-
await init(argv);
|
|
19
|
-
} else if (command === "add") {
|
|
20
|
-
await add(argv);
|
|
21
|
-
} else if (command === "doctor") {
|
|
22
|
-
await doctor(argv);
|
|
23
|
-
} else {
|
|
24
|
-
printHelp();
|
|
25
|
-
if (command !== "help" && command !== "--help" && command !== "-h") process.exitCode = 1;
|
|
26
|
-
}
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error(`jev: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
-
process.exitCode = 1;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function install(argv) {
|
|
33
|
-
const options = parseOptions(argv);
|
|
34
|
-
const project = projectPath(options);
|
|
35
|
-
const result = await writeInitialConfig(project, options.force);
|
|
36
|
-
console.log(`jev-layer ${result.created ? "initialized" : "already initialized"} at ${project}`);
|
|
37
|
-
console.log("Runtime dependencies: none. Node.js >= 20 is required.");
|
|
38
|
-
console.log("Next: jev add <omp|hermes|codex|generic> && jev doctor");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function init(argv) {
|
|
42
|
-
const options = parseOptions(argv);
|
|
43
|
-
const project = projectPath(options);
|
|
44
|
-
const result = await writeInitialConfig(project, options.force);
|
|
45
|
-
console.log(JSON.stringify({ ok: true, project, created: result.created, config: result.configPath, env_example: result.envPath }, null, 2));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function add(argv) {
|
|
49
|
-
const harness = argv[0];
|
|
50
|
-
if (!harness || ["--help", "-h"].includes(harness)) {
|
|
51
|
-
console.error("Usage: jev add <omp|hermes|codex|generic> [--project DIR] [--command CMD]");
|
|
52
|
-
process.exitCode = 1;
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const options = parseOptions(argv.slice(1));
|
|
56
|
-
const project = projectPath(options);
|
|
57
|
-
const { config } = await loadConfig({ cwd: project });
|
|
58
|
-
const provider = configuredProvider(config, options.provider);
|
|
59
|
-
const commandName = options.command ?? process.env.JEV_COMMAND ?? "jev";
|
|
60
|
-
const server = {
|
|
61
|
-
type: "stdio",
|
|
62
|
-
command: commandName,
|
|
63
|
-
args: ["mcp"],
|
|
64
|
-
cwd: ".",
|
|
65
|
-
env: { JEV_LAYER_PROVIDER: provider },
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
let destination;
|
|
69
|
-
let mode = "snippet";
|
|
70
|
-
if (harness === "omp") {
|
|
71
|
-
destination = await addOmp(project, server);
|
|
72
|
-
mode = destination.endsWith(".omp/mcp.json") ? "config" : "snippet";
|
|
73
|
-
} else if (harness === "codex") {
|
|
74
|
-
destination = await addCodex(project, server);
|
|
75
|
-
mode = destination.endsWith(".codex/config.toml") ? "config" : "snippet";
|
|
76
|
-
} else if (harness === "hermes") {
|
|
77
|
-
destination = await writeSnippet(project, "hermes", hermesSnippet(server));
|
|
78
|
-
} else if (harness === "generic" || harness === "mcp") {
|
|
79
|
-
destination = await writeSnippet(project, "generic", JSON.stringify({ mcpServers: { jev: server } }, null, 2) + "\n", ".json");
|
|
80
|
-
} else {
|
|
81
|
-
throw new Error(`unsupported harness: ${harness}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
console.log(JSON.stringify({ ok: true, harness, provider, destination, mode, command: commandName }, null, 2));
|
|
85
|
-
if (mode === "snippet") console.log("Merge this adapter snippet into the harness configuration; existing files were not overwritten.");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function doctor(argv) {
|
|
89
|
-
const options = parseOptions(argv);
|
|
90
|
-
const project = projectPath(options);
|
|
91
|
-
const { config, path: configPath, error: configError } = await loadConfig({ cwd: project });
|
|
92
|
-
const provider = configuredProvider(config, options.provider);
|
|
93
|
-
const checks = [];
|
|
94
|
-
const fail = (name, detail) => checks.push({ name, status: "error", detail });
|
|
95
|
-
const pass = (name, detail) => checks.push({ name, status: "ok", detail });
|
|
96
|
-
const warn = (name, detail) => checks.push({ name, status: "warning", detail });
|
|
97
|
-
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
98
|
-
|
|
99
|
-
if (nodeMajor >= 20) pass("node", process.versions.node);
|
|
100
|
-
else fail("node", `Node.js >= 20 required; found ${process.versions.node}`);
|
|
101
|
-
for (const relative of ["src/route.mjs", "src/cli.mjs", "src/mcp-server.mjs", "bin/jev.mjs"]) {
|
|
102
|
-
if (await pathExists(join(PACKAGE_ROOT, relative))) pass(`package:${relative}`, "present");
|
|
103
|
-
else fail(`package:${relative}`, "missing from installation");
|
|
104
|
-
}
|
|
105
|
-
if (configError) fail("config", configError);
|
|
106
|
-
else if (await pathExists(configPath)) pass("config", configPath);
|
|
107
|
-
else warn("config", `not initialized at ${configPath}; run jev init`);
|
|
108
|
-
|
|
109
|
-
if (!["demo", "openrouter", "typesafe"].includes(provider)) fail("provider", `unsupported provider ${provider}`);
|
|
110
|
-
else pass("provider", provider);
|
|
111
|
-
if (provider === "openrouter" || provider === "typesafe") {
|
|
112
|
-
const keyEnv = config.providers?.[provider]?.api_key_env ?? (provider === "openrouter" ? "OPENROUTER_API_KEY" : "TYPESAFE_API_KEY");
|
|
113
|
-
if (process.env[keyEnv]) pass(`secret:${keyEnv}`, "configured through environment");
|
|
114
|
-
else fail(`secret:${keyEnv}`, `missing; export ${keyEnv} before using ${provider}`);
|
|
115
|
-
} else {
|
|
116
|
-
pass("provider-secret", "not required for demo provider");
|
|
117
|
-
}
|
|
118
|
-
if (process.env.JEV_LAYER_ENABLED === "0") warn("fail-open", "JEV_LAYER_ENABLED=0; routing will return fallback decisions");
|
|
119
|
-
|
|
120
|
-
const errors = checks.filter((check) => check.status === "error");
|
|
121
|
-
console.log(JSON.stringify({ ok: errors.length === 0, project, package_root: PACKAGE_ROOT, checks }, null, 2));
|
|
122
|
-
if (errors.length) process.exitCode = 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function writeInitialConfig(project, force = false) {
|
|
126
|
-
const state = join(project, ".jev");
|
|
127
|
-
await mkdir(state, { recursive: true });
|
|
128
|
-
const configPath = join(state, "config.json");
|
|
129
|
-
const envPath = join(state, "providers.env.example");
|
|
130
|
-
const config = await readPackageFile("config/jev.example.json");
|
|
131
|
-
const envExample = await readPackageFile("config/providers.env.example");
|
|
132
|
-
const configCreated = await writeIfAllowed(configPath, config, force);
|
|
133
|
-
const envCreated = await writeIfAllowed(envPath, envExample, force);
|
|
134
|
-
return { created: configCreated || envCreated, configPath, envPath };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function addOmp(project, server) {
|
|
138
|
-
const target = join(project, ".omp", "mcp.json");
|
|
139
|
-
if (!(await pathExists(target))) {
|
|
140
|
-
await writeJson(target, { "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json", mcpServers: { jev: server } });
|
|
141
|
-
return target;
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
const parsed = JSON.parse(await readFile(target, "utf8"));
|
|
145
|
-
parsed.mcpServers = { ...(parsed.mcpServers ?? {}), jev: server };
|
|
146
|
-
await writeFile(target, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
147
|
-
return target;
|
|
148
|
-
} catch {
|
|
149
|
-
return writeSnippet(project, "omp", JSON.stringify({ mcpServers: { jev: server } }, null, 2) + "\n", ".json");
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function addCodex(project, server) {
|
|
154
|
-
const target = join(project, ".codex", "config.toml");
|
|
155
|
-
if (!(await pathExists(target))) {
|
|
156
|
-
await mkdir(dirname(target), { recursive: true });
|
|
157
|
-
await writeFile(target, codexSnippet(server), "utf8");
|
|
158
|
-
return target;
|
|
159
|
-
}
|
|
160
|
-
return writeSnippet(project, "codex", codexSnippet(server), ".toml");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function writeSnippet(project, name, content, extension = ".yaml") {
|
|
164
|
-
const destination = join(project, ".jev", "adapters", `${name}${extension}`);
|
|
165
|
-
await mkdir(dirname(destination), { recursive: true });
|
|
166
|
-
await writeFile(destination, content, "utf8");
|
|
167
|
-
return destination;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function codexSnippet(server) {
|
|
171
|
-
return `[mcp_servers.jev]\ncommand = ${JSON.stringify(server.command)}\nargs = ["mcp"]\ncwd = "."\n\n[mcp_servers.jev.env]\nJEV_LAYER_PROVIDER = ${JSON.stringify(server.env.JEV_LAYER_PROVIDER)}\n`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function hermesSnippet(server) {
|
|
175
|
-
return `mcp_servers:\n jev:\n command: ${server.command}\n args:\n - mcp\n env:\n JEV_LAYER_PROVIDER: ${server.env.JEV_LAYER_PROVIDER}\n # Export OPENROUTER_API_KEY or TYPESAFE_API_KEY in the Hermes process environment.\n`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function writeJson(path, value) {
|
|
179
|
-
await mkdir(dirname(path), { recursive: true });
|
|
180
|
-
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function writeIfAllowed(path, content, force) {
|
|
184
|
-
if (!force && await pathExists(path)) return false;
|
|
185
|
-
await mkdir(dirname(path), { recursive: true });
|
|
186
|
-
await writeFile(path, content, "utf8");
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async function readPackageFile(relative) {
|
|
191
|
-
return readFile(join(PACKAGE_ROOT, relative), "utf8");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function projectPath(options) {
|
|
195
|
-
return resolve(options.project ?? process.cwd());
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function parseOptions(argv) {
|
|
199
|
-
const options = {};
|
|
200
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
201
|
-
const value = argv[index];
|
|
202
|
-
if (value === "--project") options.project = argv[++index];
|
|
203
|
-
else if (value === "--command") options.command = argv[++index];
|
|
204
|
-
else if (value === "--provider") options.provider = argv[++index];
|
|
205
|
-
else if (value === "--force") options.force = true;
|
|
206
|
-
else if (value === "--help" || value === "-h") options.help = true;
|
|
207
|
-
else throw new Error(`unknown option ${value}`);
|
|
208
|
-
}
|
|
209
|
-
return options;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function printHelp() {
|
|
213
|
-
console.log(`jev-layer portable setup\n\nCommands:\n jev install [--project DIR] initialize local state\n jev init [--project DIR] write .jev/config.json and env example\n jev add <harness> [options] add omp, hermes, codex, or generic MCP adapter\n jev doctor [--project DIR] check runtime, config, provider and secrets\n jev mcp run the stdio MCP server\n jev cli run the JSONL CLI adapter\n\nOptions:\n --project DIR target workspace; defaults to cwd\n --command CMD MCP command; defaults to jev\n --provider NAME override provider for generated config\n --force replace generated init files\n`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { configuredProvider, loadConfig, pathExists } from "../src/config.mjs";
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const [command = "help", ...argv] = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
if (command === "mcp") {
|
|
12
|
+
await import("../src/mcp-server.mjs");
|
|
13
|
+
} else if (command === "cli") {
|
|
14
|
+
await import("../src/cli.mjs");
|
|
15
|
+
} else if (command === "install") {
|
|
16
|
+
await install(argv);
|
|
17
|
+
} else if (command === "init") {
|
|
18
|
+
await init(argv);
|
|
19
|
+
} else if (command === "add") {
|
|
20
|
+
await add(argv);
|
|
21
|
+
} else if (command === "doctor") {
|
|
22
|
+
await doctor(argv);
|
|
23
|
+
} else {
|
|
24
|
+
printHelp();
|
|
25
|
+
if (command !== "help" && command !== "--help" && command !== "-h") process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(`jev: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function install(argv) {
|
|
33
|
+
const options = parseOptions(argv);
|
|
34
|
+
const project = projectPath(options);
|
|
35
|
+
const result = await writeInitialConfig(project, options.force);
|
|
36
|
+
console.log(`jev-layer ${result.created ? "initialized" : "already initialized"} at ${project}`);
|
|
37
|
+
console.log("Runtime dependencies: none. Node.js >= 20 is required.");
|
|
38
|
+
console.log("Next: jev add <omp|hermes|codex|generic> && jev doctor");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function init(argv) {
|
|
42
|
+
const options = parseOptions(argv);
|
|
43
|
+
const project = projectPath(options);
|
|
44
|
+
const result = await writeInitialConfig(project, options.force);
|
|
45
|
+
console.log(JSON.stringify({ ok: true, project, created: result.created, config: result.configPath, env_example: result.envPath }, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function add(argv) {
|
|
49
|
+
const harness = argv[0];
|
|
50
|
+
if (!harness || ["--help", "-h"].includes(harness)) {
|
|
51
|
+
console.error("Usage: jev add <omp|hermes|codex|generic> [--project DIR] [--command CMD]");
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const options = parseOptions(argv.slice(1));
|
|
56
|
+
const project = projectPath(options);
|
|
57
|
+
const { config } = await loadConfig({ cwd: project });
|
|
58
|
+
const provider = configuredProvider(config, options.provider);
|
|
59
|
+
const commandName = options.command ?? process.env.JEV_COMMAND ?? "jev";
|
|
60
|
+
const server = {
|
|
61
|
+
type: "stdio",
|
|
62
|
+
command: commandName,
|
|
63
|
+
args: ["mcp"],
|
|
64
|
+
cwd: ".",
|
|
65
|
+
env: { JEV_LAYER_PROVIDER: provider },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let destination;
|
|
69
|
+
let mode = "snippet";
|
|
70
|
+
if (harness === "omp") {
|
|
71
|
+
destination = await addOmp(project, server);
|
|
72
|
+
mode = destination.endsWith(".omp/mcp.json") ? "config" : "snippet";
|
|
73
|
+
} else if (harness === "codex") {
|
|
74
|
+
destination = await addCodex(project, server);
|
|
75
|
+
mode = destination.endsWith(".codex/config.toml") ? "config" : "snippet";
|
|
76
|
+
} else if (harness === "hermes") {
|
|
77
|
+
destination = await writeSnippet(project, "hermes", hermesSnippet(server));
|
|
78
|
+
} else if (harness === "generic" || harness === "mcp") {
|
|
79
|
+
destination = await writeSnippet(project, "generic", JSON.stringify({ mcpServers: { jev: server } }, null, 2) + "\n", ".json");
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error(`unsupported harness: ${harness}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(JSON.stringify({ ok: true, harness, provider, destination, mode, command: commandName }, null, 2));
|
|
85
|
+
if (mode === "snippet") console.log("Merge this adapter snippet into the harness configuration; existing files were not overwritten.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function doctor(argv) {
|
|
89
|
+
const options = parseOptions(argv);
|
|
90
|
+
const project = projectPath(options);
|
|
91
|
+
const { config, path: configPath, error: configError } = await loadConfig({ cwd: project });
|
|
92
|
+
const provider = configuredProvider(config, options.provider);
|
|
93
|
+
const checks = [];
|
|
94
|
+
const fail = (name, detail) => checks.push({ name, status: "error", detail });
|
|
95
|
+
const pass = (name, detail) => checks.push({ name, status: "ok", detail });
|
|
96
|
+
const warn = (name, detail) => checks.push({ name, status: "warning", detail });
|
|
97
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
98
|
+
|
|
99
|
+
if (nodeMajor >= 20) pass("node", process.versions.node);
|
|
100
|
+
else fail("node", `Node.js >= 20 required; found ${process.versions.node}`);
|
|
101
|
+
for (const relative of ["src/route.mjs", "src/cli.mjs", "src/mcp-server.mjs", "bin/jev.mjs"]) {
|
|
102
|
+
if (await pathExists(join(PACKAGE_ROOT, relative))) pass(`package:${relative}`, "present");
|
|
103
|
+
else fail(`package:${relative}`, "missing from installation");
|
|
104
|
+
}
|
|
105
|
+
if (configError) fail("config", configError);
|
|
106
|
+
else if (await pathExists(configPath)) pass("config", configPath);
|
|
107
|
+
else warn("config", `not initialized at ${configPath}; run jev init`);
|
|
108
|
+
|
|
109
|
+
if (!["demo", "openrouter", "typesafe"].includes(provider)) fail("provider", `unsupported provider ${provider}`);
|
|
110
|
+
else pass("provider", provider);
|
|
111
|
+
if (provider === "openrouter" || provider === "typesafe") {
|
|
112
|
+
const keyEnv = config.providers?.[provider]?.api_key_env ?? (provider === "openrouter" ? "OPENROUTER_API_KEY" : "TYPESAFE_API_KEY");
|
|
113
|
+
if (process.env[keyEnv]) pass(`secret:${keyEnv}`, "configured through environment");
|
|
114
|
+
else fail(`secret:${keyEnv}`, `missing; export ${keyEnv} before using ${provider}`);
|
|
115
|
+
} else {
|
|
116
|
+
pass("provider-secret", "not required for demo provider");
|
|
117
|
+
}
|
|
118
|
+
if (process.env.JEV_LAYER_ENABLED === "0") warn("fail-open", "JEV_LAYER_ENABLED=0; routing will return fallback decisions");
|
|
119
|
+
|
|
120
|
+
const errors = checks.filter((check) => check.status === "error");
|
|
121
|
+
console.log(JSON.stringify({ ok: errors.length === 0, project, package_root: PACKAGE_ROOT, checks }, null, 2));
|
|
122
|
+
if (errors.length) process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function writeInitialConfig(project, force = false) {
|
|
126
|
+
const state = join(project, ".jev");
|
|
127
|
+
await mkdir(state, { recursive: true });
|
|
128
|
+
const configPath = join(state, "config.json");
|
|
129
|
+
const envPath = join(state, "providers.env.example");
|
|
130
|
+
const config = await readPackageFile("config/jev.example.json");
|
|
131
|
+
const envExample = await readPackageFile("config/providers.env.example");
|
|
132
|
+
const configCreated = await writeIfAllowed(configPath, config, force);
|
|
133
|
+
const envCreated = await writeIfAllowed(envPath, envExample, force);
|
|
134
|
+
return { created: configCreated || envCreated, configPath, envPath };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function addOmp(project, server) {
|
|
138
|
+
const target = join(project, ".omp", "mcp.json");
|
|
139
|
+
if (!(await pathExists(target))) {
|
|
140
|
+
await writeJson(target, { "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json", mcpServers: { jev: server } });
|
|
141
|
+
return target;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(await readFile(target, "utf8"));
|
|
145
|
+
parsed.mcpServers = { ...(parsed.mcpServers ?? {}), jev: server };
|
|
146
|
+
await writeFile(target, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
147
|
+
return target;
|
|
148
|
+
} catch {
|
|
149
|
+
return writeSnippet(project, "omp", JSON.stringify({ mcpServers: { jev: server } }, null, 2) + "\n", ".json");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function addCodex(project, server) {
|
|
154
|
+
const target = join(project, ".codex", "config.toml");
|
|
155
|
+
if (!(await pathExists(target))) {
|
|
156
|
+
await mkdir(dirname(target), { recursive: true });
|
|
157
|
+
await writeFile(target, codexSnippet(server), "utf8");
|
|
158
|
+
return target;
|
|
159
|
+
}
|
|
160
|
+
return writeSnippet(project, "codex", codexSnippet(server), ".toml");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function writeSnippet(project, name, content, extension = ".yaml") {
|
|
164
|
+
const destination = join(project, ".jev", "adapters", `${name}${extension}`);
|
|
165
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
166
|
+
await writeFile(destination, content, "utf8");
|
|
167
|
+
return destination;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function codexSnippet(server) {
|
|
171
|
+
return `[mcp_servers.jev]\ncommand = ${JSON.stringify(server.command)}\nargs = ["mcp"]\ncwd = "."\n\n[mcp_servers.jev.env]\nJEV_LAYER_PROVIDER = ${JSON.stringify(server.env.JEV_LAYER_PROVIDER)}\n`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function hermesSnippet(server) {
|
|
175
|
+
return `mcp_servers:\n jev:\n command: ${server.command}\n args:\n - mcp\n env:\n JEV_LAYER_PROVIDER: ${server.env.JEV_LAYER_PROVIDER}\n # Export OPENROUTER_API_KEY or TYPESAFE_API_KEY in the Hermes process environment.\n`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function writeJson(path, value) {
|
|
179
|
+
await mkdir(dirname(path), { recursive: true });
|
|
180
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function writeIfAllowed(path, content, force) {
|
|
184
|
+
if (!force && await pathExists(path)) return false;
|
|
185
|
+
await mkdir(dirname(path), { recursive: true });
|
|
186
|
+
await writeFile(path, content, "utf8");
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function readPackageFile(relative) {
|
|
191
|
+
return readFile(join(PACKAGE_ROOT, relative), "utf8");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function projectPath(options) {
|
|
195
|
+
return resolve(options.project ?? process.cwd());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseOptions(argv) {
|
|
199
|
+
const options = {};
|
|
200
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
201
|
+
const value = argv[index];
|
|
202
|
+
if (value === "--project") options.project = argv[++index];
|
|
203
|
+
else if (value === "--command") options.command = argv[++index];
|
|
204
|
+
else if (value === "--provider") options.provider = argv[++index];
|
|
205
|
+
else if (value === "--force") options.force = true;
|
|
206
|
+
else if (value === "--help" || value === "-h") options.help = true;
|
|
207
|
+
else throw new Error(`unknown option ${value}`);
|
|
208
|
+
}
|
|
209
|
+
return options;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printHelp() {
|
|
213
|
+
console.log(`jev-layer portable setup\n\nCommands:\n jev install [--project DIR] initialize local state\n jev init [--project DIR] write .jev/config.json and env example\n jev add <harness> [options] add omp, hermes, codex, or generic MCP adapter\n jev doctor [--project DIR] check runtime, config, provider and secrets\n jev mcp run the stdio MCP server\n jev cli run the JSONL CLI adapter\n\nOptions:\n --project DIR target workspace; defaults to cwd\n --command CMD MCP command; defaults to jev\n --provider NAME override provider for generated config\n --force replace generated init files\n`);
|
|
214
214
|
}
|
package/config/codex.mcp.toml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
[mcp_servers.jev]
|
|
2
|
-
command = "jev"
|
|
3
|
-
args = ["mcp"]
|
|
4
|
-
cwd = "."
|
|
5
|
-
|
|
6
|
-
[mcp_servers.jev.env]
|
|
7
|
-
JEV_LAYER_PROVIDER = "openrouter"
|
|
8
|
-
# Export OPENROUTER_API_KEY in the Codex process environment.
|
|
1
|
+
[mcp_servers.jev]
|
|
2
|
+
command = "jev"
|
|
3
|
+
args = ["mcp"]
|
|
4
|
+
cwd = "."
|
|
5
|
+
|
|
6
|
+
[mcp_servers.jev.env]
|
|
7
|
+
JEV_LAYER_PROVIDER = "openrouter"
|
|
8
|
+
# Export OPENROUTER_API_KEY in the Codex process environment.
|
package/config/generic-mcp.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
{
|
|
2
|
-
"mcpServers": {
|
|
3
|
-
"jev": {
|
|
4
|
-
"type": "stdio",
|
|
5
|
-
"command": "jev",
|
|
6
|
-
"args": ["mcp"],
|
|
7
|
-
"cwd": ".",
|
|
8
|
-
"env": {
|
|
9
|
-
"JEV_LAYER_PROVIDER": "demo"
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"jev": {
|
|
4
|
+
"type": "stdio",
|
|
5
|
+
"command": "jev",
|
|
6
|
+
"args": ["mcp"],
|
|
7
|
+
"cwd": ".",
|
|
8
|
+
"env": {
|
|
9
|
+
"JEV_LAYER_PROVIDER": "demo"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/config/hermes.mcp.yaml
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# Merge this block into the Hermes config under mcp_servers.
|
|
2
|
-
mcp_servers:
|
|
3
|
-
jev:
|
|
4
|
-
command: jev
|
|
5
|
-
args:
|
|
6
|
-
- mcp
|
|
7
|
-
env:
|
|
8
|
-
JEV_LAYER_PROVIDER: openrouter
|
|
9
|
-
# Export OPENROUTER_API_KEY in the Hermes process environment.
|
|
1
|
+
# Merge this block into the Hermes config under mcp_servers.
|
|
2
|
+
mcp_servers:
|
|
3
|
+
jev:
|
|
4
|
+
command: jev
|
|
5
|
+
args:
|
|
6
|
+
- mcp
|
|
7
|
+
env:
|
|
8
|
+
JEV_LAYER_PROVIDER: openrouter
|
|
9
|
+
# Export OPENROUTER_API_KEY in the Hermes process environment.
|
package/config/jev.example.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": 1,
|
|
3
|
-
"provider": "demo",
|
|
4
|
-
"replay_cases": ".jev/replay/cases.jsonl",
|
|
5
|
-
"features": {
|
|
6
|
-
"browser_fast_path": false,
|
|
7
|
-
"supervision": false,
|
|
8
|
-
"context_filter": null
|
|
9
|
-
},
|
|
10
|
-
"providers": {
|
|
11
|
-
"openrouter": {
|
|
12
|
-
"api_key_env": "OPENROUTER_API_KEY",
|
|
13
|
-
"endpoint_env": "OPENROUTER_DECISIONS_ENDPOINT",
|
|
14
|
-
"model_env": "OPENROUTER_DECISIONS_MODEL"
|
|
15
|
-
},
|
|
16
|
-
"typesafe": {
|
|
17
|
-
"api_key_env": "TYPESAFE_API_KEY",
|
|
18
|
-
"endpoint_env": "TYPESAFE_ENDPOINT",
|
|
19
|
-
"model_env": "TYPESAFE_MODEL"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"provider": "demo",
|
|
4
|
+
"replay_cases": ".jev/replay/cases.jsonl",
|
|
5
|
+
"features": {
|
|
6
|
+
"browser_fast_path": false,
|
|
7
|
+
"supervision": false,
|
|
8
|
+
"context_filter": null
|
|
9
|
+
},
|
|
10
|
+
"providers": {
|
|
11
|
+
"openrouter": {
|
|
12
|
+
"api_key_env": "OPENROUTER_API_KEY",
|
|
13
|
+
"endpoint_env": "OPENROUTER_DECISIONS_ENDPOINT",
|
|
14
|
+
"model_env": "OPENROUTER_DECISIONS_MODEL"
|
|
15
|
+
},
|
|
16
|
+
"typesafe": {
|
|
17
|
+
"api_key_env": "TYPESAFE_API_KEY",
|
|
18
|
+
"endpoint_env": "TYPESAFE_ENDPOINT",
|
|
19
|
+
"model_env": "TYPESAFE_MODEL"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/config/omp.mcp.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
|
|
3
|
-
"mcpServers": {
|
|
4
|
-
"jev": {
|
|
5
|
-
"type": "stdio",
|
|
6
|
-
"command": "jev",
|
|
7
|
-
"args": ["mcp"],
|
|
8
|
-
"cwd": ".",
|
|
9
|
-
"env": {
|
|
10
|
-
"JEV_LAYER_PROVIDER": "openrouter"
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
|
|
3
|
+
"mcpServers": {
|
|
4
|
+
"jev": {
|
|
5
|
+
"type": "stdio",
|
|
6
|
+
"command": "jev",
|
|
7
|
+
"args": ["mcp"],
|
|
8
|
+
"cwd": ".",
|
|
9
|
+
"env": {
|
|
10
|
+
"JEV_LAYER_PROVIDER": "openrouter"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# Copy the variables needed by the selected provider into the harness environment.
|
|
2
|
-
# Do not commit a file containing real values.
|
|
3
|
-
|
|
4
|
-
JEV_LAYER_PROVIDER=demo
|
|
5
|
-
# JEV_LAYER_PROVIDER=openrouter
|
|
6
|
-
# OPENROUTER_API_KEY=replace-in-your-shell-only
|
|
7
|
-
# OPENROUTER_DECISIONS_ENDPOINT=https://openrouter.ai/api/alpha/decisions
|
|
8
|
-
# OPENROUTER_DECISIONS_MODEL=typesafe/jev-1.13
|
|
9
|
-
|
|
10
|
-
# JEV_LAYER_PROVIDER=typesafe
|
|
11
|
-
# TYPESAFE_API_KEY=replace-in-your-shell-only
|
|
12
|
-
# TYPESAFE_ENDPOINT=https://api.typesafe.ai/v1/systemone
|
|
13
|
-
# TYPESAFE_MODEL=jev-latest
|
|
1
|
+
# Copy the variables needed by the selected provider into the harness environment.
|
|
2
|
+
# Do not commit a file containing real values.
|
|
3
|
+
|
|
4
|
+
JEV_LAYER_PROVIDER=demo
|
|
5
|
+
# JEV_LAYER_PROVIDER=openrouter
|
|
6
|
+
# OPENROUTER_API_KEY=replace-in-your-shell-only
|
|
7
|
+
# OPENROUTER_DECISIONS_ENDPOINT=https://openrouter.ai/api/alpha/decisions
|
|
8
|
+
# OPENROUTER_DECISIONS_MODEL=typesafe/jev-1.13
|
|
9
|
+
|
|
10
|
+
# JEV_LAYER_PROVIDER=typesafe
|
|
11
|
+
# TYPESAFE_API_KEY=replace-in-your-shell-only
|
|
12
|
+
# TYPESAFE_ENDPOINT=https://api.typesafe.ai/v1/systemone
|
|
13
|
+
# TYPESAFE_MODEL=jev-latest
|