@bobobowis/jev-kit 0.0.0-stage → 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 +21 -0
- package/README.md +240 -2
- package/dist/capabilities.d.ts +33 -0
- package/dist/capabilities.js +81 -0
- package/dist/cli/ask.d.ts +9 -0
- package/dist/cli/ask.js +210 -0
- package/dist/cli/common.d.ts +38 -0
- package/dist/cli/common.js +40 -0
- package/dist/cli/hook.d.ts +3 -0
- package/dist/cli/hook.js +82 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +41 -0
- package/dist/cli/mcp.d.ts +3 -0
- package/dist/cli/mcp.js +47 -0
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.js +79 -0
- package/dist/compact/compact-on-demand.d.ts +22 -0
- package/dist/compact/compact-on-demand.js +29 -0
- package/dist/compact/evaluate.d.ts +36 -0
- package/dist/compact/evaluate.js +31 -0
- package/dist/compact/index.d.ts +7 -0
- package/dist/compact/index.js +7 -0
- package/dist/compact/pick-cut-point.d.ts +18 -0
- package/dist/compact/pick-cut-point.js +29 -0
- package/dist/compact/should-compact.d.ts +57 -0
- package/dist/compact/should-compact.js +87 -0
- package/dist/core/client.d.ts +93 -0
- package/dist/core/client.js +295 -0
- package/dist/core/helpers.d.ts +14 -0
- package/dist/core/helpers.js +9 -0
- package/dist/core/mock.d.ts +20 -0
- package/dist/core/mock.js +132 -0
- package/dist/core/types.d.ts +102 -0
- package/dist/core/types.js +82 -0
- package/dist/events/index.d.ts +44 -0
- package/dist/events/index.js +77 -0
- package/dist/files/ask-file-bool.d.ts +12 -0
- package/dist/files/ask-file-bool.js +15 -0
- package/dist/files/ask-file-choice.d.ts +10 -0
- package/dist/files/ask-file-choice.js +17 -0
- package/dist/files/ask-file-score.d.ts +12 -0
- package/dist/files/ask-file-score.js +15 -0
- package/dist/files/ask-files.d.ts +23 -0
- package/dist/files/ask-files.js +56 -0
- package/dist/files/ask.d.ts +12 -0
- package/dist/files/ask.js +28 -0
- package/dist/files/assemble.d.ts +52 -0
- package/dist/files/assemble.js +129 -0
- package/dist/files/decide.d.ts +10 -0
- package/dist/files/decide.js +1 -0
- package/dist/files/index.d.ts +14 -0
- package/dist/files/index.js +14 -0
- package/dist/files/pick-first.d.ts +16 -0
- package/dist/files/pick-first.js +24 -0
- package/dist/files/prune.d.ts +13 -0
- package/dist/files/prune.js +87 -0
- package/dist/files/read-state.d.ts +17 -0
- package/dist/files/read-state.js +47 -0
- package/dist/guards/bash-gate.d.ts +29 -0
- package/dist/guards/bash-gate.js +35 -0
- package/dist/guards/index.d.ts +6 -0
- package/dist/guards/index.js +6 -0
- package/dist/guards/result-screen.d.ts +20 -0
- package/dist/guards/result-screen.js +31 -0
- package/dist/guards/write-gate.d.ts +18 -0
- package/dist/guards/write-gate.js +44 -0
- package/dist/hooks/handle.d.ts +62 -0
- package/dist/hooks/handle.js +48 -0
- package/dist/hooks/harness.d.ts +40 -0
- package/dist/hooks/harness.js +116 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/state.d.ts +8 -0
- package/dist/hooks/state.js +25 -0
- package/dist/hooks/transcript.d.ts +8 -0
- package/dist/hooks/transcript.js +52 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/ledger.d.ts +22 -0
- package/dist/ledger.js +26 -0
- package/dist/mcp/server.d.ts +28 -0
- package/dist/mcp/server.js +124 -0
- package/dist/policy/index.d.ts +17 -0
- package/dist/policy/index.js +30 -0
- package/dist/setup/index.d.ts +67 -0
- package/dist/setup/index.js +209 -0
- package/dist/tools/ask-jev.d.ts +7 -0
- package/dist/tools/ask-jev.js +42 -0
- package/dist/tools/compact-tool.d.ts +2 -0
- package/dist/tools/compact-tool.js +33 -0
- package/dist/tools/description.d.ts +5 -0
- package/dist/tools/description.js +20 -0
- package/dist/tools/file-tools.d.ts +7 -0
- package/dist/tools/file-tools.js +127 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/run-command.d.ts +7 -0
- package/dist/tools/run-command.js +27 -0
- package/dist/tools/sets.d.ts +9 -0
- package/dist/tools/sets.js +32 -0
- package/dist/tools/types.d.ts +51 -0
- package/dist/tools/types.js +4 -0
- package/package.json +96 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BLOCK_NOTICE, gateBashCommand } from "../guards/bash-gate.js";
|
|
2
|
+
import { gateWriteCall } from "../guards/write-gate.js";
|
|
3
|
+
import { screenToolResult } from "../guards/result-screen.js";
|
|
4
|
+
import { evaluateCompaction } from "../compact/evaluate.js";
|
|
5
|
+
export const ALL_GUARDS = ["bash", "write", "result"];
|
|
6
|
+
const DEFAULT_HOOK = {
|
|
7
|
+
shell: "tool_call", write: "tool_call", "read-result": "tool_result", "shell-result": "tool_result", "turn-end": "turn_end",
|
|
8
|
+
};
|
|
9
|
+
/** The text the agent reads when a guard blocks. */
|
|
10
|
+
export const blockMessage = (what, reason) => `jev-guard blocked this ${what}: ${reason}. ${BLOCK_NOTICE}`;
|
|
11
|
+
export async function handleHook(event, ctx) {
|
|
12
|
+
const on = (g) => (ctx.guards ?? ALL_GUARDS).includes(g);
|
|
13
|
+
const hook = event.hook ?? DEFAULT_HOOK[event.kind];
|
|
14
|
+
try {
|
|
15
|
+
switch (event.kind) {
|
|
16
|
+
case "shell": {
|
|
17
|
+
if (!on("bash"))
|
|
18
|
+
return {};
|
|
19
|
+
const d = await gateBashCommand(event.command, event.cwd, (s, q) => ctx.decide(`${hook} ${event.tool}`, s, q));
|
|
20
|
+
ctx.report("hook", { hook, tool: event.tool, command: event.command, block: d.block, reason: d.reason });
|
|
21
|
+
return d.block ? { block: true, reason: blockMessage("command", d.reason) } : {};
|
|
22
|
+
}
|
|
23
|
+
case "write": {
|
|
24
|
+
if (!on("write"))
|
|
25
|
+
return {};
|
|
26
|
+
const d = await gateWriteCall(event.path, event.content, event.cwd, (s, q) => ctx.decide(`${hook} ${event.tool}`, s, q));
|
|
27
|
+
ctx.report("hook", { hook, tool: event.tool, path: event.path, block: d.block, reason: d.reason });
|
|
28
|
+
return d.block ? { block: true, reason: blockMessage(event.tool, d.reason) } : {};
|
|
29
|
+
}
|
|
30
|
+
case "read-result":
|
|
31
|
+
case "shell-result": {
|
|
32
|
+
if (!on("result"))
|
|
33
|
+
return {};
|
|
34
|
+
const d = await screenToolResult(event.tool, event.text, (s, q) => ctx.decide(`${hook} ${event.tool}`, s, q));
|
|
35
|
+
ctx.report("hook", { hook, tool: event.tool, flag: d.flag, noul: d.noul, banner: d.banner });
|
|
36
|
+
return d.flag && d.banner ? { banner: d.banner } : {};
|
|
37
|
+
}
|
|
38
|
+
case "turn-end": {
|
|
39
|
+
const evaluation = await evaluateCompaction(event.input, hook, ctx.decide, ctx.report, event.action);
|
|
40
|
+
return evaluation.message ? { inject: evaluation.message, evaluation } : { evaluation };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
ctx.report("error", { hook, message: err?.message ?? String(err) });
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code and Codex hooks over the neutral handlers. Both harnesses send the same payload
|
|
3
|
+
* fields and read the same `hookSpecificOutput` responses (docs/NOTES-claude.md, NOTES-codex.md);
|
|
4
|
+
* they differ in tool names and where context usage lives.
|
|
5
|
+
*
|
|
6
|
+
* PreToolUse Bash: bash gate. Write, Edit, MultiEdit, NotebookEdit, apply_patch: write gate. Deny on block.
|
|
7
|
+
* PostToolUse Read, Bash: result screen. The banner goes back as additionalContext.
|
|
8
|
+
* UserPromptSubmit Level 7: record the request; inject a waiting tier message once.
|
|
9
|
+
* Stop Level 7: the four questions, the tier saved for the next prompt.
|
|
10
|
+
* PreCompact reported only.
|
|
11
|
+
*/
|
|
12
|
+
import type { Decider } from "../events/index.ts";
|
|
13
|
+
import type { Lines } from "../compact/should-compact.ts";
|
|
14
|
+
import { type Guard, type HookContext } from "./handle.ts";
|
|
15
|
+
export type Harness = "claude" | "codex";
|
|
16
|
+
/** Lines for real sessions, whose system prompt alone passes the lab's demo lines. */
|
|
17
|
+
export declare const HOOK_LINES: Lines;
|
|
18
|
+
export interface HarnessHookOptions {
|
|
19
|
+
harness: Harness;
|
|
20
|
+
guards?: Guard[];
|
|
21
|
+
/** Run the Level 7 advisor on UserPromptSubmit and Stop. */
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
lines?: Lines;
|
|
24
|
+
/** The model's context window in tokens, when the transcript does not say. */
|
|
25
|
+
window?: number | null;
|
|
26
|
+
decide: Decider;
|
|
27
|
+
report: HookContext["report"];
|
|
28
|
+
}
|
|
29
|
+
/** The JSON the harness reads on stdout, or null for "allow, add nothing". */
|
|
30
|
+
export type HookResponse = {
|
|
31
|
+
hookSpecificOutput: Record<string, unknown>;
|
|
32
|
+
} | null;
|
|
33
|
+
interface WriteTarget {
|
|
34
|
+
path: string;
|
|
35
|
+
content: string;
|
|
36
|
+
}
|
|
37
|
+
/** apply_patch's targets: every Add, Update, or Delete File section with its added lines. */
|
|
38
|
+
export declare function patchTargets(patch: string): WriteTarget[];
|
|
39
|
+
export declare function runHarnessHook(event: string, payload: any, opts: HarnessHookOptions): Promise<HookResponse>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ALL_GUARDS, handleHook } from "./handle.js";
|
|
2
|
+
import { readState, stateDir, writeState } from "./state.js";
|
|
3
|
+
import { claudeUsage, codexUsage } from "./transcript.js";
|
|
4
|
+
/** Lines for real sessions, whose system prompt alone passes the lab's demo lines. */
|
|
5
|
+
export const HOOK_LINES = { notice: 50_000, recommend: 100_000, request: 150_000 };
|
|
6
|
+
/** apply_patch's targets: every Add, Update, or Delete File section with its added lines. */
|
|
7
|
+
export function patchTargets(patch) {
|
|
8
|
+
const out = [];
|
|
9
|
+
let current = null;
|
|
10
|
+
for (const line of patch.split("\n")) {
|
|
11
|
+
const m = line.match(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/) ?? line.match(/^\*\*\* Move to: (.+)$/);
|
|
12
|
+
if (m) {
|
|
13
|
+
current = { path: m[1].trim(), content: "" };
|
|
14
|
+
out.push(current);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (current && line.startsWith("+"))
|
|
18
|
+
current.content += line.slice(1) + "\n";
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function writeTargets(tool, input) {
|
|
23
|
+
switch (tool) {
|
|
24
|
+
case "Write": return [{ path: String(input?.file_path ?? ""), content: String(input?.content ?? "") }];
|
|
25
|
+
case "Edit": return [{ path: String(input?.file_path ?? ""), content: String(input?.new_string ?? "") }];
|
|
26
|
+
case "MultiEdit": return [{ path: String(input?.file_path ?? ""), content: (input?.edits ?? []).map((e) => String(e?.new_string ?? "")).join("\n") }];
|
|
27
|
+
case "NotebookEdit": return [{ path: String(input?.notebook_path ?? ""), content: String(input?.new_source ?? "") }];
|
|
28
|
+
case "apply_patch": return patchTargets(String(input?.command ?? input?.patch ?? input?.input ?? ""));
|
|
29
|
+
default: return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function resultText(tool, response) {
|
|
33
|
+
if (tool === "Read")
|
|
34
|
+
return typeof response === "string" ? response : String(response?.file?.content ?? "");
|
|
35
|
+
if (tool === "Bash") {
|
|
36
|
+
if (typeof response === "string")
|
|
37
|
+
return response;
|
|
38
|
+
return [response?.stdout, response?.stderr].filter(Boolean).join("\n");
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const deny = (reason) => ({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } });
|
|
43
|
+
const context = (event, text) => ({ hookSpecificOutput: { hookEventName: event, additionalContext: text } });
|
|
44
|
+
export async function runHarnessHook(event, payload, opts) {
|
|
45
|
+
const ctx = { decide: opts.decide, report: opts.report, guards: opts.guards ?? ALL_GUARDS };
|
|
46
|
+
const cwd = String(payload?.cwd || process.cwd());
|
|
47
|
+
const tool = String(payload?.tool_name ?? "");
|
|
48
|
+
const session = String(payload?.session_id ?? "default");
|
|
49
|
+
switch (event) {
|
|
50
|
+
case "PreToolUse": {
|
|
51
|
+
if (tool === "Bash") {
|
|
52
|
+
const r = await handleHook({ kind: "shell", tool, command: String(payload?.tool_input?.command ?? ""), cwd, hook: event }, ctx);
|
|
53
|
+
return r.block ? deny(r.reason) : null;
|
|
54
|
+
}
|
|
55
|
+
const targets = writeTargets(tool, payload?.tool_input);
|
|
56
|
+
if (!targets)
|
|
57
|
+
return null;
|
|
58
|
+
for (const t of targets) {
|
|
59
|
+
const r = await handleHook({ kind: "write", tool, path: t.path, content: t.content, cwd, hook: event }, ctx);
|
|
60
|
+
if (r.block)
|
|
61
|
+
return deny(r.reason);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
case "PostToolUse": {
|
|
66
|
+
const text = resultText(tool, payload?.tool_response);
|
|
67
|
+
if (text === null)
|
|
68
|
+
return null;
|
|
69
|
+
const r = await handleHook({ kind: tool === "Read" ? "read-result" : "shell-result", tool, text, cwd, hook: event }, ctx);
|
|
70
|
+
return r.banner ? context(event, r.banner) : null;
|
|
71
|
+
}
|
|
72
|
+
case "UserPromptSubmit": {
|
|
73
|
+
if (!opts.compact)
|
|
74
|
+
return null;
|
|
75
|
+
const dir = stateDir(cwd);
|
|
76
|
+
const state = readState(dir, session);
|
|
77
|
+
state.requests.push(String(payload?.prompt ?? ""));
|
|
78
|
+
const pending = state.pending;
|
|
79
|
+
state.pending = null;
|
|
80
|
+
try {
|
|
81
|
+
writeState(dir, session, state);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
opts.report("error", { hook: event, message: `cannot save state: ${err?.message ?? err}` });
|
|
85
|
+
}
|
|
86
|
+
if (pending)
|
|
87
|
+
opts.report("hook", { hook: event, injected: pending });
|
|
88
|
+
return pending ? context(event, pending) : null;
|
|
89
|
+
}
|
|
90
|
+
case "Stop": {
|
|
91
|
+
if (!opts.compact || payload?.stop_hook_active)
|
|
92
|
+
return null;
|
|
93
|
+
const dir = stateDir(cwd);
|
|
94
|
+
const state = readState(dir, session);
|
|
95
|
+
const transcript = String(payload?.transcript_path ?? "");
|
|
96
|
+
const usage = transcript ? (opts.harness === "codex" ? codexUsage(transcript, opts.window ?? null) : claudeUsage(transcript, opts.window ?? null)) : null;
|
|
97
|
+
const r = await handleHook({
|
|
98
|
+
kind: "turn-end", cwd, hook: event, action: "ask_user",
|
|
99
|
+
input: { requests: state.requests, recentTurn: String(payload?.last_assistant_message ?? ""), toolsThisTurn: [], usage, compactable: true, lines: opts.lines ?? HOOK_LINES },
|
|
100
|
+
}, ctx);
|
|
101
|
+
state.pending = r.inject ?? null;
|
|
102
|
+
try {
|
|
103
|
+
writeState(dir, session, state);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
opts.report("error", { hook: event, message: `cannot save state: ${err?.message ?? err}` });
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
case "PreCompact":
|
|
111
|
+
opts.report("hook", { hook: event, trigger: payload?.trigger ?? null, custom_instructions: payload?.custom_instructions ?? null });
|
|
112
|
+
return null;
|
|
113
|
+
default:
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SessionState {
|
|
2
|
+
requests: string[];
|
|
3
|
+
/** The non-silent tier message to inject once on the next prompt. */
|
|
4
|
+
pending: string | null;
|
|
5
|
+
}
|
|
6
|
+
export declare const stateDir: (cwd: string) => string;
|
|
7
|
+
export declare function readState(dir: string, session: string): SessionState;
|
|
8
|
+
export declare function writeState(dir: string, session: string, state: SessionState): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Level 7 hooks remember between processes: the user requests seen so far and the tier
|
|
3
|
+
* message waiting for the next prompt. One JSON file per session in JEV_STATE_DIR (default `.jev/`
|
|
4
|
+
* in the project). Unreadable state reads as empty; a write failure never fails the hook.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
export const stateDir = (cwd) => resolve(cwd, process.env.JEV_STATE_DIR?.trim() || ".jev");
|
|
9
|
+
const fileFor = (dir, session) => join(dir, `session-${session.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
|
|
10
|
+
export function readState(dir, session) {
|
|
11
|
+
try {
|
|
12
|
+
const s = JSON.parse(readFileSync(fileFor(dir, session), "utf8"));
|
|
13
|
+
return { requests: Array.isArray(s.requests) ? s.requests.map(String) : [], pending: typeof s.pending === "string" ? s.pending : null };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { requests: [], pending: null };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function writeState(dir, session, state) {
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
const file = fileFor(dir, session);
|
|
22
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
23
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
24
|
+
renameSync(tmp, file); // two hook processes never see half a file
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Usage } from "../compact/should-compact.ts";
|
|
2
|
+
export declare function claudeUsage(transcriptPath: string, window?: number | null): Usage | null;
|
|
3
|
+
/** Codex's last context reading: input tokens of the last model call, and the model's window. */
|
|
4
|
+
export declare function codexTokenCount(transcriptPath: string): {
|
|
5
|
+
tokens: number;
|
|
6
|
+
window: number | null;
|
|
7
|
+
} | null;
|
|
8
|
+
export declare function codexUsage(transcriptPath: string, window?: number | null): Usage | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context usage from a harness's own transcript, read at the end of a turn.
|
|
3
|
+
* Claude Code: the last assistant message's input plus cache tokens. The window is not recorded.
|
|
4
|
+
* Codex: the last `token_count` event, which also carries the model's context window.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
function lines(path) {
|
|
8
|
+
let text;
|
|
9
|
+
try {
|
|
10
|
+
text = readFileSync(path, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const line of text.split("\n")) {
|
|
17
|
+
if (!line.trim())
|
|
18
|
+
continue;
|
|
19
|
+
try {
|
|
20
|
+
out.push(JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
catch { /* a line still being written */ }
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
const pctOf = (tokens, window) => (window && window > 0 ? (tokens / window) * 100 : null);
|
|
27
|
+
export function claudeUsage(transcriptPath, window = null) {
|
|
28
|
+
const all = lines(transcriptPath);
|
|
29
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
30
|
+
const u = all[i]?.type === "assistant" ? all[i]?.message?.usage : null;
|
|
31
|
+
if (!u)
|
|
32
|
+
continue;
|
|
33
|
+
const tokens = (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
|
|
34
|
+
return { tokens, pct: pctOf(tokens, window) };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** Codex's last context reading: input tokens of the last model call, and the model's window. */
|
|
39
|
+
export function codexTokenCount(transcriptPath) {
|
|
40
|
+
const all = lines(transcriptPath);
|
|
41
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
42
|
+
const p = all[i]?.payload;
|
|
43
|
+
if (all[i]?.type !== "event_msg" || p?.type !== "token_count" || !p.info?.last_token_usage)
|
|
44
|
+
continue;
|
|
45
|
+
return { tokens: p.info.last_token_usage.input_tokens ?? 0, window: p.info.model_context_window ?? null };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export function codexUsage(transcriptPath, window = null) {
|
|
50
|
+
const c = codexTokenCount(transcriptPath);
|
|
51
|
+
return c ? { tokens: c.tokens, pct: pctOf(c.tokens, window ?? c.window) } : null;
|
|
52
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bobobowis/jev-kit: the client, the wire types, the question builders, and the offline mock.
|
|
3
|
+
* Everything else ships under named entry points (policy, files, guards, compact, tools, hooks, events, ledger).
|
|
4
|
+
*/
|
|
5
|
+
export * from "./core/types.ts";
|
|
6
|
+
export * from "./core/helpers.ts";
|
|
7
|
+
export * from "./core/mock.ts";
|
|
8
|
+
export * from "./core/client.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bobobowis/jev-kit: the client, the wire types, the question builders, and the offline mock.
|
|
3
|
+
* Everything else ships under named entry points (policy, files, guards, compact, tools, hooks, events, ledger).
|
|
4
|
+
*/
|
|
5
|
+
export * from "./core/types.js";
|
|
6
|
+
export * from "./core/helpers.js";
|
|
7
|
+
export * from "./core/mock.js";
|
|
8
|
+
export * from "./core/client.js";
|
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 10, option C: the spend ledger.
|
|
3
|
+
* Every ask_jev call is counted against the agent's own turn cost, so the session can say what the habit cost and what it replaced. Numbers in code: nothing here asks Jev anything.
|
|
4
|
+
*/
|
|
5
|
+
export interface JevUsage {
|
|
6
|
+
input_tokens: number;
|
|
7
|
+
output_tokens: number;
|
|
8
|
+
cost?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Jev's list price per input token when the endpoint reports no cost. Output is free. */
|
|
11
|
+
export declare const JEV_INPUT_USD_PER_M = 0.042;
|
|
12
|
+
export interface Ledger {
|
|
13
|
+
calls: number;
|
|
14
|
+
questions: number;
|
|
15
|
+
inputTokens: number;
|
|
16
|
+
outputTokens: number;
|
|
17
|
+
usd: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const emptyLedger: () => Ledger;
|
|
20
|
+
export declare function record(ledger: Ledger, usage: JevUsage | undefined, questionCount: number): Ledger;
|
|
21
|
+
/** One line the agent, or the window, can print at the end of a task. */
|
|
22
|
+
export declare function summarize(ledger: Ledger, agentUsd: number): string;
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 10, option C: the spend ledger.
|
|
3
|
+
* Every ask_jev call is counted against the agent's own turn cost, so the session can say what the habit cost and what it replaced. Numbers in code: nothing here asks Jev anything.
|
|
4
|
+
*/
|
|
5
|
+
/** Jev's list price per input token when the endpoint reports no cost. Output is free. */
|
|
6
|
+
export const JEV_INPUT_USD_PER_M = 0.042;
|
|
7
|
+
export const emptyLedger = () => ({ calls: 0, questions: 0, inputTokens: 0, outputTokens: 0, usd: 0 });
|
|
8
|
+
export function record(ledger, usage, questionCount) {
|
|
9
|
+
const input = usage?.input_tokens ?? 0;
|
|
10
|
+
const output = usage?.output_tokens ?? 0;
|
|
11
|
+
return {
|
|
12
|
+
calls: ledger.calls + 1,
|
|
13
|
+
questions: ledger.questions + questionCount,
|
|
14
|
+
inputTokens: ledger.inputTokens + input,
|
|
15
|
+
outputTokens: ledger.outputTokens + output,
|
|
16
|
+
usd: ledger.usd + (usage?.cost ?? (input * JEV_INPUT_USD_PER_M) / 1e6),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** One line the agent, or the window, can print at the end of a task. */
|
|
20
|
+
export function summarize(ledger, agentUsd) {
|
|
21
|
+
if (ledger.calls === 0)
|
|
22
|
+
return "No Jev calls this session.";
|
|
23
|
+
const ratio = ledger.usd > 0 ? Math.round(agentUsd / ledger.usd) : 0;
|
|
24
|
+
return `${ledger.calls} Jev call${ledger.calls === 1 ? "" : "s"}, ${ledger.questions} question${ledger.questions === 1 ? "" : "s"}, $${ledger.usd.toFixed(6)}` +
|
|
25
|
+
(ratio ? `, the agent's own spend was ${ratio}x that` : "");
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Decider } from "../events/index.ts";
|
|
2
|
+
import type { CompactionSource, JevTool } from "../tools/types.ts";
|
|
3
|
+
/** Protocol revisions whose tools subset this server speaks. A client's revision is echoed when known. */
|
|
4
|
+
export declare const PROTOCOL_VERSIONS: string[];
|
|
5
|
+
export interface ServerOptions {
|
|
6
|
+
tools: JevTool[];
|
|
7
|
+
cwd: string;
|
|
8
|
+
decide: Decider;
|
|
9
|
+
report(kind: string, payload: Record<string, unknown>): void;
|
|
10
|
+
version?: string;
|
|
11
|
+
}
|
|
12
|
+
type Message = {
|
|
13
|
+
jsonrpc?: string;
|
|
14
|
+
id?: string | number | null;
|
|
15
|
+
method?: string;
|
|
16
|
+
params?: any;
|
|
17
|
+
};
|
|
18
|
+
type Reply = Record<string, unknown>;
|
|
19
|
+
/** The Level 7 hooks' notes for the most recent session in the project, for should_i_compact. */
|
|
20
|
+
export declare function latestSessionSource(cwd: string): CompactionSource;
|
|
21
|
+
export declare function instructionsFor(tools: JevTool[]): string;
|
|
22
|
+
/** Handle one message. Returns the reply to send, or null for notifications. */
|
|
23
|
+
export declare function createHandler(opts: ServerOptions): (msg: Message) => Promise<Reply | null>;
|
|
24
|
+
/** Serve until stdin closes. Calls run concurrently; replies go out as each one finishes. */
|
|
25
|
+
export declare function serve(opts: ServerOptions, input?: NodeJS.ReadableStream, output?: {
|
|
26
|
+
write(s: string): unknown;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Jev tools over MCP: newline-delimited JSON-RPC 2.0 on stdio, the subset a tool server needs.
|
|
3
|
+
* initialize, notifications/initialized, tools/list, tools/call, ping. Unknown methods get -32601,
|
|
4
|
+
* unknown tools -32602, and a failing tool is an `isError` result, never a crash.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { emptyLedger, record, summarize } from "../ledger.js";
|
|
10
|
+
import { HOOK_LINES } from "../hooks/harness.js";
|
|
11
|
+
import { readState, stateDir } from "../hooks/state.js";
|
|
12
|
+
import { ASK_JEV_NUDGE } from "../tools/ask-jev.js";
|
|
13
|
+
/** Protocol revisions whose tools subset this server speaks. A client's revision is echoed when known. */
|
|
14
|
+
export const PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
15
|
+
/** The Level 7 hooks' notes for the most recent session in the project, for should_i_compact. */
|
|
16
|
+
export function latestSessionSource(cwd) {
|
|
17
|
+
return {
|
|
18
|
+
action: "ask_user",
|
|
19
|
+
input: () => {
|
|
20
|
+
const dir = stateDir(cwd);
|
|
21
|
+
let newest = null;
|
|
22
|
+
try {
|
|
23
|
+
for (const name of readdirSync(dir)) {
|
|
24
|
+
const m = name.match(/^session-(.+)\.json$/);
|
|
25
|
+
if (!m)
|
|
26
|
+
continue;
|
|
27
|
+
const at = statSync(join(dir, name)).mtimeMs;
|
|
28
|
+
if (!newest || at > newest.at)
|
|
29
|
+
newest = { name: m[1], at };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!newest)
|
|
36
|
+
return null;
|
|
37
|
+
const s = readState(dir, newest.name);
|
|
38
|
+
if (!s.requests.length)
|
|
39
|
+
return null;
|
|
40
|
+
return { requests: s.requests, recentTurn: "", toolsThisTurn: [], usage: null, compactable: true, lines: HOOK_LINES };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function instructionsFor(tools) {
|
|
45
|
+
const lines = ["Jev answers typed questions (yes or no, pick one, a score) about files, a command's output, or your own state, in about 300 ms for a fraction of a cent. Prefer it to reasoning a judgment call out yourself."];
|
|
46
|
+
if (tools.some((t) => t.name === "ask_jev"))
|
|
47
|
+
lines.push(ASK_JEV_NUDGE);
|
|
48
|
+
if (tools.some((t) => t.name.startsWith("ask_jev_file")))
|
|
49
|
+
lines.push("The ask_jev_file tools judge one file without loading it into your context; use read when you need the code itself.");
|
|
50
|
+
return lines.join("\n\n");
|
|
51
|
+
}
|
|
52
|
+
/** Handle one message. Returns the reply to send, or null for notifications. */
|
|
53
|
+
export function createHandler(opts) {
|
|
54
|
+
let ledger = emptyLedger();
|
|
55
|
+
const ctx = {
|
|
56
|
+
cwd: opts.cwd, harness: "mcp", decide: opts.decide, report: opts.report,
|
|
57
|
+
compaction: latestSessionSource(opts.cwd),
|
|
58
|
+
};
|
|
59
|
+
const ok = (id, result) => ({ jsonrpc: "2.0", id: id ?? null, result });
|
|
60
|
+
const err = (id, code, message) => ({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
|
61
|
+
return async function handle(msg) {
|
|
62
|
+
const isRequest = msg.id !== undefined && msg.id !== null;
|
|
63
|
+
switch (msg.method) {
|
|
64
|
+
case "initialize": {
|
|
65
|
+
const asked = String(msg.params?.protocolVersion ?? "");
|
|
66
|
+
return ok(msg.id, {
|
|
67
|
+
protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
68
|
+
capabilities: { tools: { listChanged: false } },
|
|
69
|
+
serverInfo: { name: "jev", version: opts.version ?? "0.0.0" },
|
|
70
|
+
instructions: instructionsFor(opts.tools),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
case "ping":
|
|
74
|
+
return isRequest ? ok(msg.id, {}) : null;
|
|
75
|
+
case "tools/list":
|
|
76
|
+
return ok(msg.id, { tools: opts.tools.map((t) => ({ name: t.name, title: t.label, description: t.description, inputSchema: t.parameters })) });
|
|
77
|
+
case "tools/call": {
|
|
78
|
+
const name = String(msg.params?.name ?? "");
|
|
79
|
+
const tool = opts.tools.find((t) => t.name === name);
|
|
80
|
+
if (!tool)
|
|
81
|
+
return err(msg.id, -32602, `Unknown tool: ${name}. This server has ${opts.tools.map((t) => t.name).join(", ")}.`);
|
|
82
|
+
const r = await tool.execute(msg.params?.arguments ?? {}, ctx);
|
|
83
|
+
if (tool.name === "ask_jev" && !r.isError) {
|
|
84
|
+
const p = r.payload;
|
|
85
|
+
ledger = record(ledger, p.usage, Object.keys(p.answers).length);
|
|
86
|
+
opts.report("ledger", { ...ledger, summary: summarize(ledger, 0) });
|
|
87
|
+
}
|
|
88
|
+
const result = { content: [{ type: "text", text: r.text }], isError: r.isError ?? false };
|
|
89
|
+
if (!r.isError && r.payload && typeof r.payload === "object" && !Array.isArray(r.payload))
|
|
90
|
+
result.structuredContent = r.payload;
|
|
91
|
+
return ok(msg.id, result);
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
if (msg.method?.startsWith("notifications/") || !isRequest)
|
|
95
|
+
return null;
|
|
96
|
+
return err(msg.id, -32601, `Method not found: ${msg.method}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Serve until stdin closes. Calls run concurrently; replies go out as each one finishes. */
|
|
101
|
+
export async function serve(opts, input = process.stdin, output = process.stdout) {
|
|
102
|
+
const handle = createHandler(opts);
|
|
103
|
+
const send = (reply) => { if (reply)
|
|
104
|
+
output.write(JSON.stringify(reply) + "\n"); };
|
|
105
|
+
const pending = new Set();
|
|
106
|
+
for await (const line of createInterface({ input, crlfDelay: Infinity })) {
|
|
107
|
+
if (!line.trim())
|
|
108
|
+
continue;
|
|
109
|
+
let msg;
|
|
110
|
+
try {
|
|
111
|
+
msg = JSON.parse(line);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const p = handle(msg)
|
|
118
|
+
.then(send)
|
|
119
|
+
.catch((e) => send(msg.id !== undefined ? { jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e?.message ?? String(e) } } : null))
|
|
120
|
+
.finally(() => pending.delete(p));
|
|
121
|
+
pending.add(p);
|
|
122
|
+
}
|
|
123
|
+
await Promise.all(pending);
|
|
124
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy helpers: the numbers in code that turn answers into decisions.
|
|
3
|
+
* Jev never sees these. Tuning means changing a number, not a prompt.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScoreAnswer } from "../core/types.ts";
|
|
6
|
+
/** Normalize a score answer to 0-1 against its own top level, so scales of different lengths compare. */
|
|
7
|
+
export declare function normalized(answer: ScoreAnswer): number;
|
|
8
|
+
/** Weighted sum of 0-1 parts. Weights are divided by their total, so they need not add up to 1. */
|
|
9
|
+
export declare function weighted<K extends string>(parts: Record<K, number>, weights: Record<K, number>): number;
|
|
10
|
+
/** The two confidence thresholds a gate reads. */
|
|
11
|
+
export declare const CONFIDENCE: {
|
|
12
|
+
readonly REVIEW_FLOOR: 0.5;
|
|
13
|
+
readonly DESTRUCTIVE_BAR: 0.9;
|
|
14
|
+
};
|
|
15
|
+
export type ConfidenceBand = "review" | "confirm" | "act";
|
|
16
|
+
/** The answer says what, confidence says whether: below the floor a human decides, above the bar it runs, in between it asks. */
|
|
17
|
+
export declare function confidenceBand(confidence: number, floor?: number, bar?: number): ConfidenceBand;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Normalize a score answer to 0-1 against its own top level, so scales of different lengths compare. */
|
|
2
|
+
export function normalized(answer) {
|
|
3
|
+
const topLevel = Object.keys(answer.legend).length - 1;
|
|
4
|
+
return topLevel === 0 ? 0 : answer.score / topLevel;
|
|
5
|
+
}
|
|
6
|
+
/** Weighted sum of 0-1 parts. Weights are divided by their total, so they need not add up to 1. */
|
|
7
|
+
export function weighted(parts, weights) {
|
|
8
|
+
const keys = Object.keys(weights);
|
|
9
|
+
const total = keys.reduce((n, k) => n + weights[k], 0);
|
|
10
|
+
if (total <= 0)
|
|
11
|
+
throw new RangeError("weights must add up to more than 0");
|
|
12
|
+
return keys.reduce((n, k) => {
|
|
13
|
+
if (typeof parts[k] !== "number")
|
|
14
|
+
throw new RangeError(`missing part "${k}"`);
|
|
15
|
+
return n + (weights[k] / total) * parts[k];
|
|
16
|
+
}, 0);
|
|
17
|
+
}
|
|
18
|
+
/** The two confidence thresholds a gate reads. */
|
|
19
|
+
export const CONFIDENCE = {
|
|
20
|
+
REVIEW_FLOOR: 0.5, // below this: don't act, ask a human
|
|
21
|
+
DESTRUCTIVE_BAR: 0.9, // above this: act without confirmation
|
|
22
|
+
};
|
|
23
|
+
/** The answer says what, confidence says whether: below the floor a human decides, above the bar it runs, in between it asks. */
|
|
24
|
+
export function confidenceBand(confidence, floor = CONFIDENCE.REVIEW_FLOOR, bar = CONFIDENCE.DESTRUCTIVE_BAR) {
|
|
25
|
+
if (confidence < floor)
|
|
26
|
+
return "review";
|
|
27
|
+
if (confidence > bar)
|
|
28
|
+
return "act";
|
|
29
|
+
return "confirm";
|
|
30
|
+
}
|