Jev based token-usage optimization tools for Claude Code workflows.

Spend fewer tokens in long Claude Code sessions—without changing how you work.
Jev-powered context compaction, model routing, and skill selection in one Claude Code plugin.
Get started · Documentation · Architecture · Contributing
<img src="assets/demo.gif" alt="optim-jev demo in Claude Code" width="100%">
<img src="assets/optim-jev.png" alt="optim-jev architecture: context compaction, model routing, and skill selection" width="100%">
Long agent sessions accumulate stale tool output, send oversized skill catalogs, and use the main model for work a smaller model could handle. optim-jev reduces that overhead at three points while keeping Claude Code's normal workflow intact.
Each tool works independently, fails open if unavailable, and includes a stats command that reports session-level cost and savings in US dollars.
| Tool | What it does | Default | Learn more |
|---|---|---|---|
| jev-compacter | Removes stale tool calls and outputs instead of summarizing away the whole conversation | On | Guide |
| jev-router | Routes file changes to the right model using project-specific rules that evolve with your workflow | Off | Guide |
| jev-skills | Sends a short, relevant skill list for each prompt instead of the entire catalog | Off | Guide |
<img src="assets/jev-compacter.png" alt="jev-compacter session statistics" width="100%">
It works automatically with /compact and auto-compaction. Inspect the current session with:
/jev-compact stats
<img src="assets/jev-router.png" alt="jev-router model routing decisions" width="100%">
/jev-route init "what matters in this project"
/jev-route on | off
/jev-route why | stats
Tell Claude to change a preference later—for example, “let haiku edit the tests”—and the project rules update with your workflow.
<img src="assets/jev-skills.png" alt="jev-skills prompt shortlist" width="100%">
/jev-skills on | off
/jev-skills scope project | global
/jev-skills why | stats | reindex
git clone https://github.com/Oqura-ai/optim-jev.git
claude plugin marketplace add ./optim-jev
claude plugin install optim-jev@optim-jev
Set the key in Claude Code under /config → TypeSafe API key, or export it before starting Claude Code:
export TYPESAFE_API_KEY="your-key"
Restart Claude Code. jev-compacter is ready immediately; enable the router and skill picker when you want them. See Getting started for every command, option, and troubleshooting step.
[!NOTE] The plugin uses only Python's standard library. Your API key is passed to child processes through the environment and is never written to project files or logs.
| Resource | What you will find |
|---|---|
| Getting started | Installation, configuration, commands, and troubleshooting |
| TinyCart demo | A short, repeatable showcase of all three tools on a toy project |
| Performance metrics | How token usage, cost, and estimated savings are calculated |
| Architecture | The TypeScript-to-Python bridge, data flow, and session logs |
| Contributing | Local setup, tests, conventions, and pull requests |
optim-jev is released under the MIT License.
Built by Oqura.ai for more efficient Claude Code workflows.
hooks/optim-jev.ts 1314 lines1import type {
2 AgentSpawnInput,
3 CommandRunInput,
4 CommandRunResult,
5 EngineInterface as Engine,
6 ModelUsage,
7 On,
8 PluginOptions,
9 PromptComposeSection,
10 PromptSubmitInput,
11 Register,
12 SessionCompactInput,
13 SessionCompactResult,
14 SessionMessage,
15 TurnCompleteInput,
16} from 'claude-code';
17import {
18 DASHBOARD_STAR_FRAME_MS,
19 DASHBOARD_STAR_ROWS,
20 dashboardStarFrame,
21 dashboardTree,
22 type CompactionOutcome,
23 type DashboardView,
24 type RouterDecision,
25 type SkillsOutcome,
26} from './dashboard';
27import { commandOutputTree } from './command-output';
28import type { Effort, Escalated, Mode, Permission, RenderAnswer, SessionRecord, SpawnAnswer, SpawnAskClaude } from './router-lib';
29import {
30 DEFAULT_ESCALATE_AFTER,
31 DEFAULT_INIT_MODEL,
32 MODEL_GONE,
33 MODES,
34 ROUTER_COMMAND,
35 ROUTER_MODULE,
36 SECTION_ID,
37 STORE_KEY,
38 USAGE,
39 lookup,
40 newTurn,
41 numberOption,
42 oneOf,
43 isRulesFile,
44 MAX_RECENT_INPUTS,
45 parseDecision,
46 relPath,
47 rememberInput,
48 writeTargets,
49} from './router-lib';
50
51type Level = 'soft' | 'hard' | 'idle' | 'manual';
52
53type KeptMessage = {
54 from: number;
55 toolUses?: { tool_use_id: string; text?: string }[];
56 toolResults?: { tool_use_id: string; text: string }[];
57};
58
59type CompactAnswer = {
60 action: 'skip' | 'apply' | 'defer' | 'summarize';
61 message: string;
62 log: string[];
63 messages?: KeptMessage[];
64 tokensAfter?: number;
65 freedTokensEst?: number;
66};
67
68type CheckAnswer = { level: Level | null };
69
70const API_KEY_OPTION = 'typesafe_api_key';
71const PYTHON_TIMEOUT_MS = 180_000;
72const DEFAULT_HARD_PERCENT = 85;
73
74function errorText(error: unknown): string {
75 return error instanceof Error ? error.message : String(error);
76}
77
78function notify($: Engine, text: string, timeoutMs = 15_000): void {
79 $.ui.log(text);
80 $.ui.toast(text, { timeoutMs });
81}
82
83async function apiKey($: Engine, options: PluginOptions): Promise<string | undefined> {
84 const configured = options[API_KEY_OPTION];
85 if (typeof configured === 'string' && configured) return configured;
86 const fromEnv = await $.env.get('TYPESAFE_API_KEY');
87 if (fromEnv) return fromEnv;
88 const env = (await $.settings.read())['env'];
89 const fromSettings =
90 env && typeof env === 'object' ? (env as Record<string, unknown>)['TYPESAFE_API_KEY'] : undefined;
91 return typeof fromSettings === 'string' && fromSettings ? fromSettings : undefined;
92}
93
94/** Options for the Python tools, the secret left out (it travels in the child's environment). */
95function toolOptions(options: PluginOptions): Record<string, unknown> {
96 const { [API_KEY_OPTION]: _secret, ...rest } = options as Record<string, unknown>;
97 return rest;
98}
99
100async function runTool<T>(
101 $: Engine,
102 options: PluginOptions,
103 module: string,
104 mode: string,
105 request: object,
106 timeoutMs = PYTHON_TIMEOUT_MS,
107): Promise<T> {
108 const python = typeof options['python'] === 'string' && options['python'] ? options['python'] : 'python';
109 const env: Record<string, string> = { PYTHONPATH: `${$.plugin.root}/src`, PYTHONIOENCODING: 'utf-8' };
110 const key = await apiKey($, options);
111 if (key) env['TYPESAFE_API_KEY'] = key;
112 const { exitCode, stdout, stderr } = await $.process.run([python, '-m', module, mode], {
113 env,
114 stdin: JSON.stringify(request),
115 timeoutMs,
116 });
117 if (exitCode !== 0) {
118 throw new Error(`${python} exited ${exitCode}: ${stderr.trim().split('\n').slice(-3).join(' | ')}`);
119 }
120 return JSON.parse(stdout) as T;
121}
122
123// ------------------------------------------------------------- usage log
124
125const LOGS_MODULE = 'optim_jev.core.logs';
126// Claude's own token counts, as the API reported them; written to logs/claude/<session_id>.jsonl
127// in one Python call when a main-loop turn ends (the hook API can't append to a file).
128let usageBuffer: Record<string, unknown>[] = [];
129
130function recordUsage(kind: string, usage: ModelUsage, fields: Record<string, unknown>): void {
131 usageBuffer.push({
132 ts: Math.round(Date.now() / 1000),
133 kind,
134 ...fields,
135 input_tokens: usage.input_tokens,
136 output_tokens: usage.output_tokens,
137 cache_read_input_tokens: usage.cache_read_input_tokens,
138 cache_creation_input_tokens: usage.cache_creation_input_tokens,
139 });
140}
141
142async function flushUsage($: Engine, options: PluginOptions): Promise<void> {
143 if (usageBuffer.length === 0) return;
144 const records = usageBuffer;
145 usageBuffer = [];
146 try {
147 await runTool($, options, LOGS_MODULE, 'claude', {
148 session_id: await $.session.id(),
149 project_dir: await $.session.root(),
150 records,
151 });
152 } catch (error) {
153 $.ui.log(`optim-jev: usage not logged (${errorText(error)})`, { to: 'debug' });
154 }
155}
156
157const TOOL_MODULE = 'optim_jev.tools.jev_compacter.tool';
158const DEFAULT_SOFT_PERCENT = 60;
159
160async function sessionFields($: Engine, options: PluginOptions) {
161 const { context } = await $.session.usage();
162 const used = context.tokens ?? Math.round(((context.percent ?? 0) / 100) * context.window);
163 return {
164 session_id: await $.session.id(),
165 project_dir: await $.session.root(),
166 usage: { used_tokens: used, window_tokens: context.window, idle_seconds: 0 },
167 options: toolOptions(options),
168 percent: context.percent ?? (100 * used) / context.window,
169 };
170}
171
172function wire(message: SessionMessage) {
173 return {
174 role: message.role,
175 text: message.text,
176 toolUses: message.toolUses.map((t) => ({
177 tool_use_id: t.tool_use_id,
178 tool: t.tool,
179 input: t.input,
180 text: t.text,
181 isError: t.isError === true,
182 })),
183 toolResults: (message.toolResults ?? []).map((r) => ({
184 tool_use_id: r.tool_use_id,
185 text: r.text,
186 isError: r.isError,
187 })),
188 };
189}
190
191/**
192 * An unchanged message is the engine's own (handle included); a changed one is rebuilt from
193 * the original without its handle, keeping only the listed tool blocks with the given texts.
194 */
195function rebuild(input: readonly SessionMessage[], kept: KeptMessage): SessionMessage {
196 const original = input[kept.from];
197 if (!original) throw new Error(`answer names message ${kept.from} of ${input.length}`);
198 if (!kept.toolUses && !kept.toolResults) return original;
199 const uses = new Map(original.toolUses.map((t) => [t.tool_use_id, t]));
200 const results = new Map((original.toolResults ?? []).map((r) => [r.tool_use_id, r]));
201 const message: SessionMessage = {
202 role: original.role,
203 text: original.text,
204 toolUses: (kept.toolUses ?? []).map(({ tool_use_id, text }) => {
205 const use = uses.get(tool_use_id);
206 if (!use) throw new Error(`unknown tool_use ${tool_use_id}`);
207 return text === undefined || text === use.text ? use : { ...use, text, result: undefined };
208 }),
209 };
210 const toolResults = (kept.toolResults ?? []).map(({ tool_use_id, text }) => {
211 const result = results.get(tool_use_id);
212 if (!result) throw new Error(`unknown tool_result ${tool_use_id}`);
213 return text === result.text ? result : { ...result, text, result: undefined };
214 });
215 if (toolResults.length > 0) message.toolResults = toolResults;
216 return message;
217}
218
219// ---------------------------------------------------------------- jev-router
220
221let routerOptions: PluginOptions = {};
222let escalateAfter = DEFAULT_ESCALATE_AFTER;
223let initModel = DEFAULT_INIT_MODEL;
224let record: SessionRecord = { sessionId: '', mode: 'off' };
225let section = '';
226let rules: Record<string, Permission> = {};
227let tiers: string[] = ['haiku', 'sonnet', 'opus'];
228let sessionAlias: string | null = null;
229let offline = false;
230let turn = newTurn();
231// Per routed subagent: the model it runs on, its effort, and the files its prompt named.
232const agentModels = new Map<string, string>();
233const agentEfforts = new Map<string, Effort>();
234const agentFiles = new Map<string, string[]>();
235// Session-wide: per file, how many levels up the next subagent starts (same effort), after errors there.
236const escalated = new Map<string, Escalated>();
237// The person's last prompts, newest last; shared by the router and the skill picker.
238let recentInputs: string[] = [];
239const DEFAULT_DECIDER_MODEL = 'haiku';
240let deciderModel = DEFAULT_DECIDER_MODEL;
241
242const isActive = () => record.mode === 'on';
243const aliasOf = (model: string | undefined | null) =>
244 model ? tiers.find((t) => model.toLowerCase().includes(t)) : undefined;
245const bump = (alias: string) => tiers[Math.min(tiers.length - 1, tiers.indexOf(alias) + 1)] ?? alias;
246
247// Dashboard snapshot. Feature state in each tool remains the source of truth.
248const DASHBOARD_STORE_KEY = 'optim_jev_dashboard_session';
249type DashboardRecord = {
250 sessionId: string;
251 contextPercent: number | null;
252 compactionOutcome: CompactionOutcome;
253 compacterDegraded: boolean;
254 routerDecision: RouterDecision | null;
255 routerOffline: boolean;
256 skillsOutcome: SkillsOutcome;
257};
258
259let pluginOptions: PluginOptions = {};
260let compacterOn = true;
261let dashboardOn = true;
262let dashboardSessionId = '';
263let dashboardActivity: string | null = null;
264let dashboardContextPercent: number | null = null;
265let compactionOutcome: CompactionOutcome = { kind: 'not_run' };
266let compacterDegraded = false;
267let routerHasRules = false;
268let routerRefreshingFailed = false;
269let routerRouting = false;
270let lastRouterDecision: RouterDecision | null = null;
271let dashboardStarFrameNumber = 0;
272let dashboardStarRequestId: string | null = null;
273let dashboardStarTimer: { cancel(): void } | null = null;
274let dashboardStarColumns = 1;
275let dashboardSaveQueue: Promise<void> = Promise.resolve();
276
277function dashboardRecord(): DashboardRecord {
278 return {
279 sessionId: dashboardSessionId,
280 contextPercent: dashboardContextPercent,
281 compactionOutcome,
282 compacterDegraded,
283 routerDecision: lastRouterDecision,
284 routerOffline: offline,
285 skillsOutcome,
286 };
287}
288
289function restoreDashboard(stored: Partial<DashboardRecord> | undefined): void {
290 if (!stored || stored.sessionId !== dashboardSessionId) return;
291 if (stored.contextPercent === null || (typeof stored.contextPercent === 'number' && Number.isFinite(stored.contextPercent))) {
292 dashboardContextPercent = stored.contextPercent;
293 }
294 if (stored.compactionOutcome?.kind) compactionOutcome = stored.compactionOutcome;
295 if (typeof stored.compacterDegraded === 'boolean') compacterDegraded = stored.compacterDegraded;
296 if (stored.routerDecision === null || stored.routerDecision?.model) lastRouterDecision = stored.routerDecision;
297 if (typeof stored.routerOffline === 'boolean') offline = stored.routerOffline;
298 if (stored.skillsOutcome?.kind) skillsOutcome = stored.skillsOutcome;
299}
300
301function persistDashboard($: Engine): void {
302 if (!dashboardSessionId) return;
303 const snapshot = dashboardRecord();
304 dashboardSaveQueue = dashboardSaveQueue
305 .catch(() => undefined)
306 .then(async () => {
307 await $.store.set(DASHBOARD_STORE_KEY, snapshot);
308 })
309 .catch((error) => {
310 $.ui.log(`optim-jev: dashboard state not saved (${errorText(error)})`, { to: 'debug' });
311 });
312}
313
314function stopDashboardStars(): void {
315 dashboardStarRequestId = null;
316 dashboardStarTimer?.cancel();
317 dashboardStarTimer = null;
318}
319
320function animateDashboardStars($: Engine, requestId: string, columns: number): void {
321 dashboardStarRequestId = requestId;
322 dashboardStarColumns = columns;
323 if (dashboardStarTimer) return;
324 try {
325 dashboardStarTimer = $.clock.every(DASHBOARD_STAR_FRAME_MS, () => {
326 const active = dashboardStarRequestId;
327 if (!active) return stopDashboardStars();
328 dashboardStarFrameNumber += 1;
329 void $.ui
330 .blit({
331 requestId: active,
332 key: 'dashboard-stars',
333 cells: dashboardStarFrame(dashboardStarFrameNumber, dashboardStarColumns, DASHBOARD_STAR_ROWS),
334 })
335 .then((result) => {
336 if ('deny' in result && result.deny) stopDashboardStars();
337 })
338 .catch(stopDashboardStars);
339 });
340 } catch {
341 // Animation is optional: keep the static particle frame and the dashboard itself.
342 dashboardStarTimer = null;
343 }
344}
345
346function refreshUi($: Engine): void {
347 persistDashboard($);
348 try {
349 if (dashboardOn) {
350 $.ui.status(undefined);
351 $.ui.invalidate('ui.render');
352 } else {
353 $.ui.status(dashboardActivity ? `◆ jev · ${dashboardActivity}` : idleStatus());
354 }
355 } catch (error) {
356 $.ui.log(`optim-jev: dashboard not refreshed (${errorText(error)})`, { to: 'debug' });
357 }
358}
359
360function showActivity($: Engine, activity: string | null): void {
361 dashboardActivity = activity;
362 refreshUi($);
363}
364
365function rememberContext(percent: number): void {
366 dashboardContextPercent = Number.isFinite(percent) ? percent : null;
367}
368
369async function rememberCompactedContext(
370 $: Engine,
371 options: PluginOptions,
372 result: SessionCompactResult,
373 knownWindow = 0,
374): Promise<void> {
375 try {
376 const fields = knownWindow > 0 ? null : await sessionFields($, options);
377 const windowTokens = knownWindow > 0 ? knownWindow : (fields?.usage.window_tokens ?? 0);
378 if (typeof result.tokensAfter === 'number' && windowTokens > 0) {
379 rememberContext((100 * result.tokensAfter) / windowTokens);
380 } else if (fields && (fields.percent > 0 || dashboardContextPercent === null)) {
381 rememberContext(fields.percent);
382 }
383 } catch {
384 // The exact outcome remains useful even when usage is temporarily unavailable.
385 }
386}
387
388function dashboardView(): DashboardView {
389 return {
390 contextPercent: dashboardContextPercent,
391 activity: dashboardActivity,
392 compacter: {
393 enabled: compacterOn,
394 softPercent: numberOption(pluginOptions, 'jev_compacter_soft_percent', DEFAULT_SOFT_PERCENT),
395 hardPercent: numberOption(pluginOptions, 'jev_compacter_hard_percent', DEFAULT_HARD_PERCENT),
396 outcome: compactionOutcome,
397 degraded: compacterDegraded,
398 },
399 router: {
400 mode: record.mode,
401 routing: routerRouting,
402 offline: offline || routerRefreshingFailed,
403 hasRules: routerHasRules,
404 decision: lastRouterDecision,
405 },
406 skills: {
407 mode: skills.mode,
408 scope: skills.scope,
409 count: skillCount,
410 indexing: skillsIndexing,
411 picking: skillsPicking,
412 outcome: skillsOutcome,
413 },
414 };
415}
416
417function idleStatus(): string {
418 const router = !isActive()
419 ? 'router off'
420 : offline
421 ? `router offline → ${sessionAlias ?? 'session model'}`
422 : 'routing subagents';
423 return `◆ jev · compact ${compacterOn ? 'on' : 'off'} · ${router} · ${skillsStatus()}`;
424}
425
426async function routerFields($: Engine) {
427 return {
428 session_id: await $.session.id(),
429 project_dir: await $.session.root(),
430 session_model: await $.session.model(),
431 options: toolOptions(routerOptions),
432 };
433}
434
435async function saveRecord($: Engine) {
436 await $.store.set(STORE_KEY, record);
437}
438
439async function refresh($: Engine) {
440 try {
441 const answer = await runTool<RenderAnswer>($, routerOptions, ROUTER_MODULE, 'render', await routerFields($));
442 section = answer.section;
443 rules = answer.rules;
444 tiers = answer.tiers.length > 0 ? answer.tiers : tiers;
445 sessionAlias = answer.current;
446 routerHasRules = answer.has_rules;
447 routerRefreshingFailed = false;
448 } catch (error) {
449 section = '';
450 routerRefreshingFailed = true;
451 $.ui.log(`jev-router: could not render its prompt section (${errorText(error)})`);
452 }
453 refreshUi($);
454}
455
456async function setMode($: Engine, mode: Mode) {
457 record = { ...record, mode };
458 await saveRecord($);
459 await refresh($);
460}
461
462async function routerSessionStart($: Engine) {
463 await $.command.register({
464 name: ROUTER_COMMAND,
465 description: 'Jev model router: on or off; init, why, stats',
466 argumentHint: '[on|off|init|why|stats]',
467 });
468 const sessionId = await $.session.id();
469 const stored = (await $.store.get(STORE_KEY)) as Partial<SessionRecord> | undefined;
470 if (stored && stored.sessionId === sessionId) {
471 record = { sessionId, mode: oneOf(MODES, stored.mode, record.mode) };
472 } else {
473 record = { ...record, sessionId };
474 await saveRecord($);
475 }
476 escalated.clear();
477 agentEfforts.clear();
478 recentInputs = await typedPrompts($);
479 await refresh($);
480}
481
482/** A resumed or reloaded session's last prompts, from the transcript: the person's own text only. */
483async function typedPrompts($: Engine): Promise<string[]> {
484 try {
485 let recent: string[] = [];
486 for (const m of await $.session.messages()) {
487 if (m.role === 'user' && !(m.toolResults?.length)) recent = rememberInput(recent, m.text);
488 }
489 return recent;
490 } catch {
491 return [];
492 }
493}
494
495async function runCommand($: Engine, e: CommandRunInput): Promise<CommandRunResult> {
496 const [verb = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean);
497 const arg = rest.join(' ');
498 try {
499 if (verb === '') return { text: `jev-router: ${record.mode}\n\n${USAGE}` };
500 if ((MODES as readonly string[]).includes(verb)) {
501 await setMode($, verb as Mode);
502 return {
503 text:
504 verb === 'on'
505 ? 'jev-router: on for this session. File changes go through subagents; Jev picks each one\'s model.'
506 : 'jev-router: off for this session.',
507 };
508 }
509 if (verb === 'why' || verb === 'stats') {
510 const { text } = await runTool<{ text: string }>($, routerOptions, ROUTER_MODULE, verb, await routerFields($));
511 return { text };
512 }
513 if (verb === 'init') return await runInit($, arg);
514 return { text: USAGE };
515 } catch (error) {
516 dashboardActivity = null;
517 refreshUi($);
518 return { text: `jev-router: ${errorText(error)}` };
519 }
520}
521
522/** A one-token call: the full id if it answers, the alias alone (id learned later), or unavailable. */
523async function checkModel(
524 $: Engine,
525 alias: string,
526 id: string,
527 sessionModel: string,
528): Promise<{ id: string; available: boolean }> {
529 if (aliasOf(sessionModel) === alias) return { id: sessionModel, available: true };
530 for (const model of id ? [id, alias] : [alias]) {
531 try {
532 const r = await $.model.complete({ model, prompt: 'Reply OK.', maxTokens: 1, timeoutMs: 30_000 });
533 if (r.isAnswered || r.reason !== 'api-error' || !MODEL_GONE.has(r.error)) {
534 return { id: model === alias ? '' : model, available: true };
535 }
536 } catch {
537 // the engine refused to send it: a blocked or unknown model
538 }
539 }
540 return { id: '', available: false };
541}
542
543async function runInit($: Engine, arg: string): Promise<CommandRunResult> {
544 const depthMatch = /--depth[ =](\d+)/.exec(arg);
545 const depth = depthMatch ? Number(depthMatch[1]) : undefined;
546 const force = /(^|\s)--force(?=\s|$)/.test(arg);
547 const directive = arg
548 .replace(/--depth[ =]\d+/, '')
549 .replace(/(^|\s)--force(?=\s|$)/, ' ')
550 .trim()
551 .replace(/^["']|["']$/g, '');
552 showActivity($, 'Scanning project for router rules…');
553 const base = await routerFields($);
554 const scan = await runTool<
555 | { refused: string }
556 | { refused?: undefined; system: string; prompt: string; paths: number; models: { alias: string; id: string }[] }
557 >($, routerOptions, ROUTER_MODULE, 'scan', { ...base, directive, depth, force });
558 if (scan.refused !== undefined) {
559 showActivity($, null);
560 return { text: scan.refused };
561 }
562 showActivity($, 'Checking model access…');
563 const checks = Object.fromEntries(
564 await Promise.all(scan.models.map(async (m) => [m.alias, await checkModel($, m.alias, m.id, base.session_model)] as const)),
565 );
566 showActivity($, `Writing router rules for ${scan.paths} paths…`);
567 const reply = await $.model.complete({
568 model: initModel,
569 system: scan.system,
570 prompt: scan.prompt,
571 maxTokens: 8_000,
572 timeoutMs: 180_000,
573 });
574 if (reply.usage) recordUsage('init', reply.usage, { model: initModel });
575 if (!reply.isAnswered) {
576 showActivity($, null);
577 return { text: `jev-router: the rules writer (${initModel}) gave no answer (${reply.reason})` };
578 }
579 const saved = await runTool<{ text: string; count: number }>($, routerOptions, ROUTER_MODULE, 'init_save', {
580 ...base,
581 directive,
582 depth,
583 reply: reply.text,
584 checks,
585 });
586 dashboardActivity = null;
587 const turnedOn = !isActive();
588 if (turnedOn) await setMode($, 'on');
589 else await refresh($);
590 return { text: saved.text + (turnedOn ? '\n\nRouting is now on for this session.' : '') };
591}
592
593function composeSections(sections: readonly PromptComposeSection[]): readonly PromptComposeSection[] {
594 if (!isActive() || !section) return sections;
595 return [...sections.filter((s) => s.id !== SECTION_ID), { id: SECTION_ID, text: section, scope: 'session' }];
596}
597
598/** The main loop may not change files; a routed subagent only those its model is allowed. */
599async function guardWrite(
600 $: Engine,
601 e: Record<string, unknown> & { agentId?: string },
602): Promise<{ deny?: string; watched: boolean }> {
603 if (!isActive()) return { watched: false };
604 const { paths, kind } = writeTargets(e);
605 if (paths.length === 0) return { watched: false };
606 const root = await $.session.root();
607 if (e.agentId === undefined) {
608 // The two routing files are the one thing the main loop may change: that is how preferences evolve.
609 if (kind === 'write' && paths.every((p) => isRulesFile(relPath(p, root)))) return { watched: false };
610 turn.deniedCount += 1;
611 return {
612 watched: false,
613 deny:
614 'jev-router: you cannot change files yourself while routing is on. Spawn a subagent with the Agent ' +
615 'tool: the goal, the exact file paths, and what done looks like. Jev picks its model.',
616 };
617 }
618 const running = agentModels.get(e.agentId);
619 if (!running) return { watched: false };
620 for (const target of paths) {
621 const rel = relPath(target, root);
622 const permission = lookup(rules, rel, tiers);
623 const allowed = kind === 'write' ? permission.write : permission.delete;
624 if (allowed.includes(running)) continue;
625 turn.deniedCount += 1;
626 const why =
627 allowed.length === 0
628 ? `no model may ${kind} ${rel}`
629 : `${running} may not ${kind} ${rel}; it needs ${allowed.join(' or ')}`;
630 notify($, `🔒 jev: ${why}`, 8_000);
631 return {
632 watched: false,
633 deny: `jev-router: ${why} (.optim-jev/jev_router/rules.json). Leave it and say so in your final report.`,
634 };
635 }
636 return { watched: true };
637}
638
639async function editedRulesFile($: Engine, e: Record<string, unknown>): Promise<boolean> {
640 const { paths, kind } = writeTargets(e);
641 if (kind !== 'write' || paths.length === 0) return false;
642 const root = await $.session.root();
643 return paths.some((p) => isRulesFile(relPath(p, root)));
644}
645
646/** A routing file changed: reload the copy guardWrite checks, and report what a hand edit broke. */
647async function afterRulesEdit($: Engine): Promise<string | undefined> {
648 await refresh($);
649 try {
650 const { problems } = await runTool<{ problems: string[] }>(
651 $,
652 routerOptions,
653 ROUTER_MODULE,
654 'validate',
655 await routerFields($),
656 );
657 if (problems.length === 0) return undefined;
658 notify($, `⚠ jev: rules.json has ${problems.length} problem(s); Claude was told`, 10_000);
659 return `jev-router: rules.json has problems. Fix them now.\n${problems.map((p) => `- ${p}`).join('\n')}`;
660 } catch (error) {
661 $.ui.log(`jev-router: rules.json not validated (${errorText(error)})`, { to: 'debug' });
662 return undefined;
663 }
664}
665
666function countWriteResult($: Engine, agentId: string | undefined, isError: boolean) {
667 if (!isError || agentId === undefined) return;
668 const errors = (turn.errors.get(agentId) ?? 0) + 1;
669 turn.errors.set(agentId, errors);
670 if (errors !== escalateAfter) return;
671 const from = agentModels.get(agentId);
672 const files = agentFiles.get(agentId) ?? [];
673 // Stops at the strongest model; errors !== escalateAfter above makes it once per subagent.
674 if (!from || files.length === 0 || bump(from) === from) return;
675 const effort = agentEfforts.get(agentId) ?? null;
676 for (const f of files) escalated.set(f, { levels: (escalated.get(f)?.levels ?? 0) + 1, effort });
677 turn.escalations.push({ from, to: bump(from), effort, files });
678 const at = effort ? ` at ${effort} effort` : '';
679 notify($, `⬆ jev: ${from} hit ${escalateAfter} errors; the next subagent for ${files.join(', ')} starts on ${bump(from)}${at}`, 8_000);
680}
681
682/** Jev's input stayed over budget: a small, low-effort Claude model decides instead. */
683async function askClaudeToRoute($: Engine, prompt: string): Promise<{ model?: string; effort?: string } | { failed: string }> {
684 try {
685 const r = await $.model.complete({ model: deciderModel, prompt, maxTokens: 100, effort: 'low', timeoutMs: 30_000 });
686 if (r.usage) recordUsage('route_decider', r.usage, { model: deciderModel });
687 if (!r.isAnswered) return { failed: `${deciderModel} gave no answer (${r.reason})` };
688 return parseDecision(r.text) ?? { failed: `${deciderModel} answered no JSON` };
689 } catch (error) {
690 return { failed: `${deciderModel}: ${errorText(error)}` };
691 }
692}
693
694async function routeSpawn(
695 $: Engine,
696 e: AgentSpawnInput,
697): Promise<{ model?: string; effort?: Effort; prompt?: string; files: string[] }> {
698 if (!isActive() || e.fork || e.isTeammate) return { files: [] };
699 routerRouting = true;
700 showActivity($, `Routing “${e.description || 'subagent'}”…`);
701 try {
702 const request = {
703 ...(await routerFields($)),
704 description: e.description,
705 prompt: e.prompt,
706 escalated: Object.fromEntries(escalated),
707 recent_inputs: recentInputs,
708 };
709 let answer = await runTool<SpawnAnswer | SpawnAskClaude>($, routerOptions, ROUTER_MODULE, 'spawn', request);
710 if ('ask_claude' in answer) {
711 showActivity($, `Routing “${e.description || 'subagent'}” with ${deciderModel}…`);
712 const decided = await askClaudeToRoute($, answer.ask_claude);
713 answer = await runTool<SpawnAnswer | SpawnAskClaude>($, routerOptions, ROUTER_MODULE, 'spawn', { ...request, decided });
714 if ('ask_claude' in answer) throw new Error('router asked for a decision twice');
715 }
716 offline = answer.jev_failed !== null;
717 turn.spawned += 1;
718 lastRouterDecision = {
719 task: e.description || 'subagent',
720 model: answer.model,
721 effort: answer.effort,
722 source: answer.source ?? (answer.jev_failed ? 'fallback' : 'jev'),
723 escalated: answer.escalated ?? 0,
724 };
725 notify($, answer.toast, 6_000);
726 return {
727 model: answer.model,
728 ...(answer.effort ? { effort: answer.effort } : {}),
729 prompt: answer.appendix ? e.prompt + answer.appendix : undefined,
730 files: answer.files,
731 };
732 } catch (error) {
733 offline = true;
734 lastRouterDecision = {
735 task: e.description || 'subagent',
736 model: sessionAlias ?? 'session',
737 source: 'session',
738 escalated: 0,
739 };
740 $.ui.log(`jev-router: subagent not routed (${errorText(error)})`, { to: 'debug' });
741 return { files: [] };
742 } finally {
743 routerRouting = false;
744 showActivity($, null);
745 }
746}
747
748async function routerTurnComplete($: Engine, e: TurnCompleteInput) {
749 if (e.agentId !== undefined) return;
750 if (isActive() && (turn.spawned > 0 || turn.escalations.length > 0 || turn.deniedCount > 0)) {
751 try {
752 await runTool($, routerOptions, ROUTER_MODULE, 'outcome', {
753 ...(await routerFields($)),
754 record: {
755 turn_id: e.turnId,
756 reason: e.reason,
757 spawned: turn.spawned,
758 errors: [...turn.errors.values()].reduce((a, b) => a + b, 0),
759 escalations: turn.escalations,
760 denials: turn.deniedCount,
761 },
762 });
763 } catch (error) {
764 $.ui.log(`jev-router: outcome not logged (${errorText(error)})`, { to: 'debug' });
765 }
766 }
767 refreshUi($);
768}
769
770/**
771 * The router's own hooks. `session.start` and `turn.complete` are shared with the compacter, whose
772 * hooks call routerSessionStart and routerTurnComplete (one unmatched hook per event per plugin).
773 */
774function registerRouter(on: On, pluginOptions: PluginOptions): void {
775 routerOptions = pluginOptions;
776 escalateAfter = numberOption(routerOptions, 'jev_router_escalate_after', DEFAULT_ESCALATE_AFTER);
777 deciderModel =
778 typeof routerOptions['jev_router_decider_model'] === 'string' && routerOptions['jev_router_decider_model']
779 ? routerOptions['jev_router_decider_model']
780 : DEFAULT_DECIDER_MODEL;
781 initModel =
782 typeof routerOptions['jev_router_init_model'] === 'string' && routerOptions['jev_router_init_model']
783 ? routerOptions['jev_router_init_model']
784 : DEFAULT_INIT_MODEL;
785 record = { sessionId: '', mode: oneOf(MODES, routerOptions['jev_router_mode'], 'off') };
786
787 on('command.run', { command: 'jev-route' }, async ($, e) => runCommand($, e));
788
789 on('prompt.compose', async ($, e, next) => {
790 const result = await next(e);
791 return { sections: composeSections(result.sections) };
792 });
793
794 on('turn.start', async ($, e, next) => {
795 turn = newTurn();
796 return next(e);
797 });
798
799 on('tool.call', { tool: ['Edit', 'Write', 'NotebookEdit', 'Bash', 'PowerShell'] }, async ($, e, next) => {
800 const input = e as unknown as Record<string, unknown> & { agentId?: string };
801 const verdict = await guardWrite($, input);
802 if (verdict.deny !== undefined) return { deny: verdict.deny };
803 const result = await next(e);
804 if (verdict.watched) countWriteResult($, e.agentId, result.isError === true);
805 if (result.deny !== undefined || result.isError === true || !(await editedRulesFile($, input))) return result;
806 const warning = await afterRulesEdit($);
807 return warning ? { ...result, context: [...(result.context ?? []), warning] } : result;
808 });
809
810 // A spawn can't set effort, so each of a routed subagent's requests gets it here. A model
811 // without effort sends none (e.effort absent), so nothing is forced on it.
812 on('turn.step', async function* ($, e, next) {
813 const effort = e.agentId !== undefined ? agentEfforts.get(e.agentId) : undefined;
814 return yield* next(effort && e.effort !== undefined && e.effort !== effort ? { ...e, effort } : e);
815 });
816
817 on('agent.spawn', async ($, e, next) => {
818 const routed = await routeSpawn($, e);
819 const result = await next({
820 ...e,
821 ...(routed.model ? { model: routed.model } : {}),
822 ...(routed.prompt ? { prompt: routed.prompt } : {}),
823 });
824 if (routed.model && result.agentId) {
825 agentModels.set(result.agentId, aliasOf(result.model) ?? routed.model);
826 agentFiles.set(result.agentId, routed.files);
827 if (routed.effort) agentEfforts.set(result.agentId, routed.effort);
828 // Ties this agent's `turn` records to the router's spawn record.
829 usageBuffer.push({
830 ts: Math.round(Date.now() / 1000),
831 kind: 'spawned',
832 agent_id: result.agentId,
833 description: e.description,
834 routed_model: routed.model,
835 effort: routed.effort ?? null,
836 model: result.model,
837 files: routed.files,
838 });
839 }
840 return result;
841 });
842}
843
844// ---------------------------------------------------------------- jev-skills
845
846const SKILLS_MODULE = 'optim_jev.tools.jev_skills.tool';
847const SKILLS_COMMAND = 'jev-skills';
848const SKILLS_STORE_KEY = 'jev_skills_session';
849const SKILLS_MODES = ['off', 'on'] as const;
850const SKILLS_SCOPES = ['project', 'global'] as const;
851const SKILLS_TIMEOUT_MS = 30_000;
852const SKILLS_USAGE = [
853 'Usage: /jev-skills [on | off]',
854 ' /jev-skills scope project|global',
855 ' /jev-skills why | stats | reindex',
856].join('\n');
857const FOLLOW_UP =
858 /^(y(es)?|no?|ok(ay)?|sure|continue|go( ahead)?|do it|proceed|thanks?|thank you|lgtm|looks good|next|done)[.! ]*$/i;
859type SkillsRecord = { sessionId: string; mode: (typeof SKILLS_MODES)[number]; scope: (typeof SKILLS_SCOPES)[number] };
860
861let skillsOptions: PluginOptions = {};
862let skills: SkillsRecord = { sessionId: '', mode: 'off', scope: 'project' };
863// Folders outside the project holding skills in scope: Claude may read them without asking.
864let skillDirs: string[] = [];
865let skillCount = 0;
866let lastPicks: string[] = [];
867let listingNote = '';
868let skillsIndexing = false;
869let skillsPicking = false;
870let skillsOutcome: SkillsOutcome = { kind: 'not_run', picks: [] };
871
872const skillsOn = () => skills.mode === 'on';
873const normPath = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
874
875function skillsStatus(): string {
876 return skillsOn() ? `skills ${skills.scope} (${skillCount})` : 'skills off';
877}
878
879async function skillsFields($: Engine) {
880 return {
881 session_id: await $.session.id(),
882 project_dir: await $.session.root(),
883 scope: skills.scope,
884 options: toolOptions(skillsOptions),
885 };
886}
887
888async function reindex($: Engine) {
889 if (!skillsOn()) {
890 skillDirs = [];
891 skillsIndexing = false;
892 return;
893 }
894 skillsIndexing = true;
895 showActivity($, `Indexing ${skills.scope} skills…`);
896 try {
897 const answer = await runTool<{ count: number; dirs: string[] }>($, skillsOptions, SKILLS_MODULE, 'index', await skillsFields($));
898 skillCount = answer.count;
899 skillDirs = answer.dirs.map(normPath);
900 if (skillsOutcome.kind === 'unavailable') skillsOutcome = { kind: 'not_run', picks: [] };
901 } catch (error) {
902 skillsOutcome = { kind: 'unavailable', picks: [] };
903 $.ui.log(`jev-skills: could not index skills (${errorText(error)})`);
904 } finally {
905 skillsIndexing = false;
906 showActivity($, null);
907 }
908}
909
910async function skillsSessionStart($: Engine) {
911 await $.command.register({
912 name: SKILLS_COMMAND,
913 description: 'Jev skill picker: on or off, project or global scope; why, stats',
914 argumentHint: '[on|off|scope project|global|why|stats|reindex]',
915 });
916 const sessionId = await $.session.id();
917 const stored = (await $.store.get(SKILLS_STORE_KEY)) as Partial<SkillsRecord> | undefined;
918 skills =
919 stored && stored.sessionId === sessionId
920 ? { sessionId, mode: oneOf(SKILLS_MODES, stored.mode, skills.mode), scope: oneOf(SKILLS_SCOPES, stored.scope, skills.scope) }
921 : { ...skills, sessionId };
922 await $.store.set(SKILLS_STORE_KEY, skills);
923 const hasListSkills = (await $.tool.list()).some((t) => t.name === 'ListSkills');
924 listingNote =
925 'Skills are not listed here: optim-jev\'s skill picker shortlists the relevant ones with each request. ' +
926 (hasListSkills
927 ? 'If you need a skill that was not shortlisted, search with the ListSkills tool.'
928 : 'The person can also run any skill with /<name>.');
929 await reindex($);
930}
931
932async function updateSkills($: Engine, change: Partial<Pick<SkillsRecord, 'mode' | 'scope'>> = {}): Promise<void> {
933 skills = { ...skills, ...change };
934 await $.store.set(SKILLS_STORE_KEY, skills);
935 await reindex($);
936 refreshUi($);
937}
938
939async function runSkillsCommand($: Engine, e: CommandRunInput): Promise<CommandRunResult> {
940 const [verb = '', arg = ''] = e.args.trim().split(/\s+/).filter(Boolean);
941 try {
942 if (verb === '') return { text: `jev-skills: ${skillsStatus()}\n\n${SKILLS_USAGE}` };
943 if ((SKILLS_MODES as readonly string[]).includes(verb) || verb === 'scope' || verb === 'reindex') {
944 if (verb === 'scope') {
945 if (!(SKILLS_SCOPES as readonly string[]).includes(arg)) return { text: SKILLS_USAGE };
946 await updateSkills($, { scope: arg as SkillsRecord['scope'] });
947 } else if (verb !== 'reindex') {
948 await updateSkills($, { mode: verb as SkillsRecord['mode'] });
949 } else {
950 await updateSkills($);
951 }
952 const extra =
953 skillsOn() && skills.scope === 'global'
954 ? `\nClaude may read files in ${skillDirs.length} skill folder(s) outside the project without asking.`
955 : '';
956 return { text: `jev-skills: ${skillsStatus()}${extra}` };
957 }
958 if (verb === 'why' || verb === 'stats') {
959 const { text } = await runTool<{ text: string }>($, skillsOptions, SKILLS_MODULE, verb, await skillsFields($));
960 return { text };
961 }
962 return { text: SKILLS_USAGE };
963 } catch (error) {
964 return { text: `jev-skills: ${errorText(error)}` };
965 }
966}
967
968/** The shortlist for one prompt, as context beside it; nothing for slash commands and short follow-ups. */
969async function shortlistFor($: Engine, e: PromptSubmitInput): Promise<string | undefined> {
970 const text = e.text.trim();
971 const kind = e.origin.kind;
972 if (!skillsOn() || skillCount === 0 || (kind !== 'composer' && kind !== 'bridge')) return undefined;
973 if (text.startsWith('/') || text.length < 12 || FOLLOW_UP.test(text)) return undefined;
974 skillsPicking = true;
975 showActivity($, `Picking skills from ${skillCount}…`);
976 try {
977 const answer = await runTool<{ picks: string[]; context: string; failed: string | null }>(
978 $,
979 skillsOptions,
980 SKILLS_MODULE,
981 'shortlist',
982 { ...(await skillsFields($)), prompt: e.text, recent_inputs: recentInputs.slice(-(MAX_RECENT_INPUTS - 1)) },
983 SKILLS_TIMEOUT_MS,
984 );
985 lastPicks = answer.picks;
986 skillsOutcome = {
987 kind: answer.failed ? 'jev_offline' : answer.picks.length > 0 ? 'picked' : 'none',
988 picks: answer.picks,
989 };
990 if (answer.picks.length > 0) notify($, `jev skills · ${answer.picks.join(', ')}`, 6_000);
991 return answer.context || undefined;
992 } catch (error) {
993 skillsOutcome = { kind: 'unavailable', picks: [] };
994 $.ui.log(`jev-skills: no shortlist (${errorText(error)})`, { to: 'debug' });
995 return undefined;
996 } finally {
997 skillsPicking = false;
998 showActivity($, null);
999 }
1000}
1001
1002function isSkillPath(input: unknown): boolean {
1003 if (!input || typeof input !== 'object') return false;
1004 const record = input as Record<string, unknown>;
1005 const raw = record['file_path'] ?? record['path'];
1006 if (typeof raw !== 'string' || !raw) return false;
1007 const path = normPath(raw);
1008 return skillDirs.some((dir) => path === dir || path.startsWith(dir + '/'));
1009}
1010
1011function registerSkills(on: On, pluginOptions: PluginOptions): void {
1012 skillsOptions = pluginOptions;
1013 skills = {
1014 sessionId: '',
1015 mode: oneOf(SKILLS_MODES, pluginOptions['jev_skills_mode'], 'off'),
1016 scope: oneOf(SKILLS_SCOPES, pluginOptions['jev_skills_scope'], 'project'),
1017 };
1018
1019 on('command.run', { command: SKILLS_COMMAND }, async ($, e) => runSkillsCommand($, e));
1020
1021 // The built-in listing of every skill goes; a fixed note takes its place so the cache keeps it.
1022 on('prompt.attachment', async ($, e, next) => {
1023 if (!skillsOn() || e.type !== 'skill_listing') return next(e);
1024 return { text: listingNote };
1025 });
1026
1027 on('prompt.submit', async ($, e, next) => {
1028 // Earlier prompts only: the current one goes to Jev as the request itself.
1029 const context = await shortlistFor($, e);
1030 if (e.origin.kind === 'composer' || e.origin.kind === 'bridge') recentInputs = rememberInput(recentInputs, e.text);
1031 return next(context ? { ...e, context: [...(e.context ?? []), context] } : e);
1032 });
1033
1034 // Global scope: reading a shortlisted skill's own files outside the project needs no prompt.
1035 on('tool.check', { tool: ['Read', 'Glob', 'Grep'] }, async ($, e, next) => {
1036 const verdict = await next(e);
1037 if (verdict.decision !== 'ask' || !skillsOn() || skills.scope !== 'global' || !isSkillPath(e.input)) return verdict;
1038 return { decision: 'allow', reason: 'optim-jev: a skill folder in the skill picker\'s global scope' };
1039 });
1040
1041 on('skill.prompt', async ($, e, next) => {
1042 const result = await next(e);
1043 if (skillsOn()) {
1044 try {
1045 await runTool($, skillsOptions, SKILLS_MODULE, 'loaded', {
1046 ...(await skillsFields($)),
1047 skill: e.skill,
1048 shortlisted: lastPicks.includes(e.skill),
1049 });
1050 } catch (error) {
1051 $.ui.log(`jev-skills: load not logged (${errorText(error)})`, { to: 'debug' });
1052 }
1053 }
1054 return result;
1055 });
1056}
1057
1058// ------------------------------------------------------------- jev-compacter
1059
1060// The level a compaction this plugin starts runs at, read by its session.compact.
1061let pendingLevel: Level | null = null;
1062let checking = false;
1063
1064/** Claude Code's own summary: its real token counts are the baseline jev-compacter is measured against. */
1065async function recordSummary(
1066 $: Engine,
1067 e: SessionCompactInput,
1068 result: SessionCompactResult,
1069 level: Level | null,
1070): Promise<void> {
1071 if (!result.messages || !result.usage) return;
1072 recordUsage('summary', result.usage, {
1073 trigger: e.trigger,
1074 level,
1075 agent_id: e.agentId ?? null,
1076 model: await $.session.model(),
1077 tokens_before: result.tokensBefore ?? null,
1078 tokens_after: result.tokensAfter ?? null,
1079 });
1080}
1081
1082async function autoCompact($: Engine, options: PluginOptions): Promise<void> {
1083 checking = true;
1084 showActivity($, 'Checking context…');
1085 try {
1086 const fields = await sessionFields($, options);
1087 rememberContext(fields.percent);
1088 const soft = options['jev_compacter_soft_percent'];
1089 if (fields.percent >= (typeof soft === 'number' ? soft : DEFAULT_SOFT_PERCENT)) {
1090 const { percent: _percent, ...request } = fields;
1091 const { level } = await runTool<CheckAnswer>($, options, TOOL_MODULE, 'check', request);
1092 compacterDegraded = false;
1093 if (level) {
1094 pendingLevel = level;
1095 await $.session.compact();
1096 }
1097 }
1098 } catch (error) {
1099 compacterDegraded = true;
1100 compactionOutcome = { kind: 'skipped' };
1101 $.ui.log(`jev-compacter: auto-compaction check skipped (${errorText(error)})`);
1102 } finally {
1103 pendingLevel = null;
1104 checking = false;
1105 showActivity($, null);
1106 }
1107}
1108
1109export const register: Register = (on: On, options: PluginOptions) => {
1110 pluginOptions = options;
1111 compacterOn = options['jev_compacter_mode'] !== 'off';
1112 dashboardOn = options['jev_dashboard_mode'] !== 'off';
1113 registerRouter(on, options);
1114 registerSkills(on, options);
1115
1116 on('command.run', { command: 'jev-compact' }, async ($, e) => {
1117 if (e.args.trim() !== 'stats') return { text: 'Usage: /jev-compact stats' };
1118 try {
1119 const { percent: _percent, ...fields } = await sessionFields($, options);
1120 const { text } = await runTool<{ text: string }>($, options, TOOL_MODULE, 'stats', fields);
1121 return { text };
1122 } catch (error) {
1123 return { text: `jev-compacter: ${errorText(error)}` };
1124 }
1125 });
1126
1127 on('session.start', async ($, e, next) => {
1128 stopDashboardStars();
1129 dashboardSessionId = await $.session.id();
1130 dashboardActivity = null;
1131 dashboardContextPercent = null;
1132 compactionOutcome = { kind: 'not_run' };
1133 compacterDegraded = false;
1134 lastRouterDecision = null;
1135 offline = false;
1136 routerHasRules = false;
1137 routerRefreshingFailed = false;
1138 routerRouting = false;
1139 lastPicks = [];
1140 skillsIndexing = false;
1141 skillsPicking = false;
1142 skillsOutcome = { kind: 'not_run', picks: [] };
1143 restoreDashboard((await $.store.get(DASHBOARD_STORE_KEY)) as Partial<DashboardRecord> | undefined);
1144 await $.command.register({
1145 name: 'jev-compact',
1146 description: "Jev compacter: this session's compactions and what they cost",
1147 argumentHint: 'stats',
1148 });
1149 await routerSessionStart($);
1150 await skillsSessionStart($);
1151 try {
1152 const livePercent = (await sessionFields($, options)).percent;
1153 if (livePercent > 0 || dashboardContextPercent === null) rememberContext(livePercent);
1154 } catch {
1155 // The dashboard can draw without a context percentage.
1156 }
1157 refreshUi($);
1158 return next(e);
1159 });
1160
1161 on('session.compact', async ($, e, next) => {
1162 if (!compacterOn || e.agentId !== undefined || e.trigger === 'precompute') {
1163 const result = await next(e);
1164 await recordSummary($, e, result, null);
1165 if (e.agentId === undefined && e.trigger !== 'precompute') {
1166 await rememberCompactedContext($, options, result);
1167 refreshUi($);
1168 }
1169 return result;
1170 }
1171 const level: Level =
1172 e.trigger === 'manual' ? 'manual' : e.trigger === 'auto' ? 'hard' : (pendingLevel ?? 'soft');
1173 pendingLevel = null;
1174 const mustShrink = level === 'manual' || level === 'hard';
1175 let contextWindow = 0;
1176 showActivity($, `Compacting${dashboardContextPercent === null ? '' : ` at ${Math.round(dashboardContextPercent)}%`}…`);
1177 try {
1178 const { percent: _percent, ...fields } = await sessionFields($, options);
1179 contextWindow = fields.usage.window_tokens;
1180 rememberContext((100 * fields.usage.used_tokens) / fields.usage.window_tokens);
1181 const messages =
1182 Array.isArray(e.messages) && e.messages.length > 0
1183 ? e.messages
1184 : await $.session.messages().catch(() => []);
1185 const answer = await runTool<CompactAnswer>($, options, TOOL_MODULE, 'compact', {
1186 ...fields,
1187 level,
1188 messages: messages.map(wire),
1189 });
1190 for (const line of answer.log) $.ui.log(line, { to: 'debug' });
1191 if (answer.message) notify($, answer.message);
1192 const freedTokens = Math.max(0, answer.freedTokensEst ?? 0);
1193 compacterDegraded = answer.message.includes('Jev failed');
1194 if (answer.action === 'apply' && answer.messages) {
1195 compactionOutcome = { kind: 'applied', freedTokensEst: freedTokens };
1196 if (answer.tokensAfter !== undefined) {
1197 rememberContext((100 * answer.tokensAfter) / fields.usage.window_tokens);
1198 }
1199 return {
1200 messages: answer.messages.map((kept) => rebuild(messages, kept)),hooks/dashboard.tsx 400 lines1/* Hallmark · component: terminal dashboard · genre: modern-minimal · theme: warm orange
2 * pre-emit critique: P5 H5 E5 S5 R5 V4 · states and focus: Claude Code Button
3 */
4export type CompactionOutcome =
5 | { kind: 'not_run' }
6 | { kind: 'applied'; freedTokensEst: number }
7 | { kind: 'deferred'; freedTokensEst: number }
8 | { kind: 'builtin' }
9 | { kind: 'nothing' }
10 | { kind: 'skipped' };
11
12export type RouterDecision = {
13 task: string;
14 model: string;
15 effort?: string | null;
16 source: 'jev' | 'rules' | 'escalation' | 'claude' | 'fallback' | 'session';
17 escalated: number;
18};
19
20export type SkillsOutcome =
21 | { kind: 'not_run'; picks: string[] }
22 | { kind: 'picked'; picks: string[] }
23 | { kind: 'none'; picks: string[] }
24 | { kind: 'jev_offline'; picks: string[] }
25 | { kind: 'unavailable'; picks: string[] };
26
27export type DashboardView = {
28 contextPercent: number | null;
29 activity: string | null;
30 compacter: {
31 enabled: boolean;
32 softPercent: number;
33 hardPercent: number;
34 outcome: CompactionOutcome;
35 degraded: boolean;
36 };
37 router: {
38 mode: 'on' | 'off';
39 routing: boolean;
40 offline: boolean;
41 hasRules: boolean;
42 decision: RouterDecision | null;
43 };
44 skills: {
45 mode: 'on' | 'off';
46 scope: 'project' | 'global';
47 count: number;
48 indexing: boolean;
49 picking: boolean;
50 outcome: SkillsOutcome;
51 };
52};
53
54export type DashboardActions = {
55 compact: () => unknown;
56 toggleRouter: () => unknown;
57 toggleSkills: () => unknown;
58};
59
60const ACCENT = '#d97757';
61const WARNING = '#f0b35f';
62const BACKGROUND = '#191411';
63export const DASHBOARD_STAR_COLUMNS = 40;
64export const DASHBOARD_STAR_ROWS = 3;
65export const DASHBOARD_STAR_FRAME_MS = 220;
66export const DASHBOARD_STAR_SPACING = 11;
67
68const particleNoise = (seed: number) => {
69 const value = Math.sin(seed * 12.9898) * 43758.5453;
70 return value - Math.floor(value);
71};
72
73const smoothstep = (value: number) => value * value * (3 - 2 * value);
74
75/** One independently timed particle per horizontal zone; each respawn gets a fresh position. */
76export function dashboardStarPoints(columns: number, rows: number, frame: number) {
77 const count = Math.ceil(columns / DASHBOARD_STAR_SPACING);
78 return Array.from({ length: count }, (_, slot) => {
79 const start = slot * DASHBOARD_STAR_SPACING;
80 const width = Math.min(DASHBOARD_STAR_SPACING, columns - start);
81 const seed = slot * 47 + 11;
82 const lifetime = 18 + Math.floor(particleNoise(seed + 5) * 22);
83 const shifted = frame + Math.floor(particleNoise(seed + 17) * lifetime);
84 const run = Math.floor(shifted / lifetime);
85 const age = (shifted % lifetime) / lifetime;
86 const cycleSeed = seed + run * 197;
87 const glow =
88 age < 0.2
89 ? smoothstep(age / 0.2)
90 : age < 0.58
91 ? 1
92 : age < 0.82
93 ? smoothstep((0.82 - age) / 0.24)
94 : 0;
95 return {
96 x: start + Math.floor(particleNoise(cycleSeed) * Math.max(1, width)),
97 y: (slot + run + Math.floor(particleNoise(cycleSeed + 29) * rows)) % rows,
98 glow: glow * (0.45 + particleNoise(cycleSeed + 73) * 0.55),
99 };
100 });
101}
102
103/** A sparse terminal-native field whose independent particles fade, rest, and respawn. */
104export function dashboardStarFrame(
105 frame: number,
106 columns = DASHBOARD_STAR_COLUMNS,
107 rows = DASHBOARD_STAR_ROWS,
108): string {
109 const words = new Uint32Array(columns * rows * 3);
110 const particles = new Map(
111 dashboardStarPoints(columns, rows, frame).map((particle) => [particle.y * columns + particle.x, particle]),
112 );
113 for (let row = 0; row < rows; row++)
114 for (let x = 0; x < columns; x++) {
115 const i = (row * columns + x) * 3;
116 const particle = particles.get(row * columns + x);
117 const glow = particle?.glow ?? 0;
118 words[i] = glow > 0.16 ? 0x00b7 : 0x20;
119 words[i + 1] = glow > 0.68 ? 0xd97757 : 0x513327;
120 words[i + 2] = 0x191411;
121 }
122 return new Uint8Array(words.buffer).toBase64();
123}
124
125const DASHBOARD_STARS_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="60" viewBox="0 0 1600 60" preserveAspectRatio="none"><style>.s{fill:${ACCENT};opacity:0;animation:tw 7s ease-in-out infinite}@keyframes tw{0%,100%{opacity:0}18%{opacity:.42}58%{opacity:.18}78%{opacity:0}}@media(prefers-reduced-motion:reduce){.s{animation:none;opacity:.14}}</style><circle class="s" cx="34" cy="12" r=".8" style="animation-duration:5.4s;animation-delay:-1.1s"/><circle class="s" cx="146" cy="43" r=".8" style="animation-duration:8.1s;animation-delay:-5.3s"/><circle class="s" cx="278" cy="20" r=".8" style="animation-duration:6.7s;animation-delay:-2.7s"/><circle class="s" cx="404" cy="49" r=".8" style="animation-duration:9.3s;animation-delay:-7.4s"/><circle class="s" cx="538" cy="9" r=".8" style="animation-duration:7.6s;animation-delay:-3.1s"/><circle class="s" cx="674" cy="35" r=".8" style="animation-duration:5.9s;animation-delay:-4.8s"/><circle class="s" cx="818" cy="16" r=".8" style="animation-duration:10.2s;animation-delay:-2.2s"/><circle class="s" cx="956" cy="47" r=".8" style="animation-duration:6.3s;animation-delay:-.9s"/><circle class="s" cx="1094" cy="25" r=".8" style="animation-duration:8.8s;animation-delay:-6.5s"/><circle class="s" cx="1232" cy="8" r=".8" style="animation-duration:7.1s;animation-delay:-1.5s"/><circle class="s" cx="1374" cy="41" r=".8" style="animation-duration:9.7s;animation-delay:-8.9s"/><circle class="s" cx="1542" cy="18" r=".8" style="animation-duration:5.7s;animation-delay:-.3s"/></svg>`;
126
127function title(value: string): string {
128 return value ? value[0]!.toUpperCase() + value.slice(1) : value;
129}
130
131function short(value: string, limit: number): string {
132 const one = value.replace(/\s+/g, ' ').trim();
133 return one.length <= limit ? one : `${one.slice(0, limit - 1)}…`;
134}
135
136function tokens(value: number): string {
137 return value >= 1000 ? `~${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k` : `~${value}`;
138}
139
140export function compacterText(view: DashboardView, _narrow = false): string {
141 const c = view.compacter;
142 if (!c.enabled) return 'Compacter off';
143 const percent = view.contextPercent === null ? '—' : `${Math.round(view.contextPercent)}%`;
144 const threshold = `soft ${c.softPercent}% · hard ${c.hardPercent}%`;
145 const outcome =
146 c.outcome.kind === 'applied'
147 ? `freed ${tokens(c.outcome.freedTokensEst)}`
148 : c.outcome.kind === 'deferred'
149 ? `deferred ${tokens(c.outcome.freedTokensEst)}`
150 : c.outcome.kind === 'builtin'
151 ? 'built-in summary'
152 : c.outcome.kind === 'nothing'
153 ? 'nothing to prune'
154 : c.outcome.kind === 'skipped'
155 ? 'skipped'
156 : '';
157 return [`Context ${percent}`, threshold, outcome].filter(Boolean).join(' · ');
158}
159
160export function routerText(view: DashboardView, narrow = false): string {
161 const r = view.router;
162 if (r.mode === 'off') return 'Router off';
163 if (r.routing) return 'Router routing…';
164 if (r.offline && !r.decision) return 'Router offline · session model';
165 if (!r.decision) {
166 if (!r.hasRules) return 'Router on · no rules · run /jev-route init';
167 return 'Router on';
168 }
169 const up = r.decision.escalated ? ` ↑${r.decision.escalated}` : '';
170 const effort = r.decision.effort ? ` · ${r.decision.effort}` : '';
171 if (narrow) return `Router on → ${title(r.decision.model)}${effort}${up}`;
172 if (r.decision.source === 'fallback' || r.decision.source === 'session') {
173 return `Router on · ${short(r.decision.task, 22)} → ${title(r.decision.model)}${effort} · session model · Jev offline${up}`;
174 }
175 const source = r.decision.source === 'jev' ? 'Jev' : r.decision.source === 'claude' ? 'Claude' : r.decision.source;
176 return `Router on · ${short(r.decision.task, 22)} → ${title(r.decision.model)}${effort} via ${source}${up}`;
177}
178
179export function skillsText(view: DashboardView, narrow = false): string {
180 const s = view.skills;
181 if (s.mode === 'off') return 'Skills off';
182 if (s.indexing) return 'Skills indexing…';
183 if (s.picking) return narrow ? 'Skills picking…' : `Skills picking from ${s.count}…`;
184 const base = `Skills ${title(s.scope)} ${s.count}`;
185 if (s.outcome.kind === 'unavailable') return `${base} · picker unavailable`;
186 if (s.outcome.kind === 'jev_offline') return `${base} · Jev offline · skill names attached`;
187 if (s.outcome.kind === 'none') return `${base} · none matched`;
188 if (s.outcome.kind !== 'picked') return base;
189 const [first, second] = s.outcome.picks;
190 const picked = second ? `${first} +${s.outcome.picks.length - 1}` : first;
191 return `${base} · ${short(picked ?? '', narrow ? 16 : 24)}`;
192}
193
194type Elements = {
195 // Claude Code supplies these components for the surface being rendered.
196 // eslint-disable-next-line @typescript-eslint/no-explicit-any
197 Box: any;
198 // eslint-disable-next-line @typescript-eslint/no-explicit-any
199 Text: any;
200 // eslint-disable-next-line @typescript-eslint/no-explicit-any
201 Button: any;
202 // eslint-disable-next-line @typescript-eslint/no-explicit-any
203 Raster?: any;
204 // eslint-disable-next-line @typescript-eslint/no-explicit-any
205 Svg?: any;
206};
207
208/** The host can reduce AbovePrompt's height independently of terminal width. */
209export function dashboardLayout(columns: number, maxRows = 5) {
210 const width = Number.isFinite(columns) ? Math.max(0, Math.floor(columns)) : 0;
211 const rows = Number.isFinite(maxRows) ? Math.max(0, Math.floor(maxRows)) : 0;
212 return {
213 width,
214 rows,
215 full: width >= 60 && rows >= 5,
216 controls: width >= 50 && rows >= 2,
217 };
218}
219
220export function dashboardTree(
221 elements: Elements,
222 view: DashboardView,
223 actions: DashboardActions,
224 columns: number,
225 terminal = false,
226 starFrame = 0,
227 maxRows = 5,
228) {
229 const { Box, Text, Button } = elements;
230 const layout = dashboardLayout(columns, maxRows);
231 // No border or fixed-size decoration when the host cannot fit the five-row card.
232 // Commands remain available when there is no room for buttons.
233 if (terminal && !layout.full) {
234 if (!layout.width || !layout.rows) return null;
235 const percent = view.contextPercent === null ? '?' : `${Math.round(view.contextPercent)}%`;
236 const compact = view.compacter.enabled ? `Ctx ${percent}` : 'Compact off';
237 const router = view.router.mode === 'off'
238 ? 'off' : view.router.routing ? 'routing' : view.router.offline ? 'offline' : 'on';
239 const skills = view.skills.mode === 'off'
240 ? 'off' : view.skills.indexing ? 'indexing' : view.skills.picking ? 'picking' : 'on';
241 const status = `${compact} | Router ${router} | Skills ${skills}`;
242 return (
243 <Box
244 key="optim-jev-dashboard"
245 flexDirection="column"
246 width={layout.width}
247 height={layout.controls ? 2 : 1}
248 overflow="hidden"
249 >
250 <Box height={1} minWidth={0} overflow="hidden">
251 <Text wrap="truncate">{view.activity ? `${status} | ${view.activity}` : status}</Text>
252 </Box>
253 {layout.controls ? (
254 <Box
255 key="dashboard-actions"
256 flexDirection="row"
257 flexWrap="nowrap"
258 gap={1}
259 height={1}
260 minWidth={0}
261 overflow="hidden"
262 >
263 <Button key="dashboard-compact" plain label="Compact" onPress={actions.compact} />
264 <Button
265 key="dashboard-router"
266 plain
267 label={`Router ${view.router.mode}`}
268 onPress={actions.toggleRouter}
269 />
270 <Button
271 key="dashboard-skills"
272 plain
273 label={`Skills ${view.skills.mode}`}
274 onPress={actions.toggleSkills}
275 />
276 </Box>
277 ) : null}
278 </Box>
279 );
280 }
281 const narrow = columns > 0 && columns < 90;
282 const compactWarning = view.compacter.degraded;
283 const routerWarning = view.router.mode === 'on' && view.router.offline;
284 const skillsWarning = view.skills.outcome.kind === 'jev_offline' || view.skills.outcome.kind === 'unavailable';
285 const brand = (
286 <Box flexShrink={0}>
287 <Text color={ACCENT} bold>
288 ✦ optim-jev
289 </Text>
290 </Box>
291 );
292 const compactControl = (
293 <Button key="dashboard-compact" variant="secondary" label="Compact" onPress={actions.compact} />
294 );
295 const routerControl = (
296 <Button
297 key="dashboard-router"
298 plain
299 dimColor={view.router.mode === 'off'}
300 label={`${view.router.mode === 'on' ? 'Disable' : 'Enable'} router`}
301 onPress={actions.toggleRouter}
302 />
303 );
304 const skillsControl = (
305 <Button
306 key="dashboard-skills"
307 plain
308 dimColor={view.skills.mode === 'off'}
309 label={`${view.skills.mode === 'on' ? 'Disable' : 'Enable'} skills`}
310 onPress={actions.toggleSkills}
311 />
312 );
313 const controls = (
314 <Box key="dashboard-actions" flexShrink={0} flexDirection="row" flexWrap="wrap" gap={1} alignItems="center">
315 {compactControl}
316 {routerControl}
317 {skillsControl}
318 </Box>
319 );
320 const starColumns = Math.max(1, columns - 4);
321 const terminalStars =
322 terminal && columns >= 60 && elements.Raster ? (
323 <Box key="dashboard-star-field" position="absolute" top={0} left={0} right={0} bottom={0} overflow="hidden">
324 <elements.Raster
325 key="dashboard-stars"
326 columns={starColumns}
327 rows={DASHBOARD_STAR_ROWS}
328 cells={dashboardStarFrame(starFrame, starColumns, DASHBOARD_STAR_ROWS)}
329 />
330 </Box>
331 ) : null;
332 const desktopStars =
333 !terminal && elements.Svg ? (
334 <Box key="dashboard-star-field" position="absolute" top={0} right={0} bottom={0}>
335 <elements.Svg source={DASHBOARD_STARS_SVG} alt="" width={420} height={60} />
336 </Box>
337 ) : null;
338 return (
339 <Box
340 key="optim-jev-dashboard"
341 position="relative"
342 flexDirection="column"
343 paddingX={1}
344 overflow="hidden"
345 backgroundColor={BACKGROUND}
346 borderStyle="round"
347 >
348 {desktopStars}
349 {terminalStars}
350 {terminal ? (
351 <Box flexDirection="column" overflow="hidden">
352 <Box flexDirection="row" gap={1} alignItems="center">
353 {brand}
354 <Text dimColor>·</Text>
355 <Box flexGrow={1} flexShrink={1} minWidth={0} overflow="hidden">
356 <Text color={compactWarning ? WARNING : undefined} wrap="truncate">
357 {view.activity ?? compacterText(view)}
358 </Text>
359 </Box>
360 {compactControl}
361 </Box>
362 <Box flexDirection="row" gap={1} alignItems="center">
363 <Box flexGrow={1} flexShrink={1} minWidth={0} overflow="hidden">
364 <Text color={routerWarning ? WARNING : undefined} wrap="truncate">{routerText(view)}</Text>
365 </Box>
366 {routerControl}
367 </Box>
368 <Box flexDirection="row" gap={1} alignItems="center">
369 <Box flexGrow={1} flexShrink={1} minWidth={0} overflow="hidden">
370 <Text color={skillsWarning ? WARNING : undefined} wrap="truncate">{skillsText(view)}</Text>
371 </Box>
372 {skillsControl}
373 </Box>
374 </Box>
375 ) : (
376 <Box flexDirection="column">
377 <Box flexDirection="row" gap={1} alignItems="center">
378 {brand}
379 <Box flexGrow={1} flexShrink={1} minWidth={0} overflow="hidden">
380 {view.activity ? (
381 <Text color={ACCENT} wrap="truncate">
382 {view.activity}
383 </Text>
384 ) : null}
385 </Box>
386 {controls}
387 </Box>
388 <Text wrap="truncate">
389 <Text color={compactWarning ? WARNING : undefined}>{compacterText(view, narrow)}</Text>
390 <Text dimColor>{' │ '}</Text>
391 <Text color={routerWarning ? WARNING : undefined}>{routerText(view, narrow)}</Text>
392 <Text dimColor>{' │ '}</Text>
393 <Text color={skillsWarning ? WARNING : undefined}>{skillsText(view, narrow)}</Text>
394 </Text>
395 </Box>
396 )}
397 </Box>
398 );
399}
400hooks/command-output.tsx 113 lines1/** Styled transcript cards for optim-jev's diagnostic slash commands. */
2
3type Elements = {
4 // Claude Code supplies these constructors for the active render surface.
5 // eslint-disable-next-line @typescript-eslint/no-explicit-any
6 Box: any;
7 // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 Text: any;
9};
10
11const TITLES: Record<string, Partial<Record<string, string>>> = {
12 'jev-route': { stats: 'Router stats', why: 'Why this route' },
13 'jev-skills': { stats: 'Skills stats', why: 'Why these skills' },
14 'jev-compact': { stats: 'Compacter stats' },
15};
16
17const TOKEN = /(saved|applied|shortlist(?:ed)?|measured)/gi;
18const GOOD = /^(saved|applied|shortlist(?:ed)?|measured)$/i;
19
20function valueParts(Text: Elements['Text'], line: string) {
21 return line.split(TOKEN).filter(Boolean).map((part, index) => {
22 return GOOD.test(part) ? <Text key={`part-${index}`} bold>{part}</Text> : part;
23 });
24}
25
26function outputText(Text: Elements['Text'], line: string, _isErrored: boolean) {
27 const note = /^(Estimate prices|Prices |Listing and note sizes|No routed subagent turns)/.test(line);
28
29 return (
30 <Text wrap="wrap" dimColor={note}>
31 {valueParts(Text, line)}
32 </Text>
33 );
34}
35
36type OutputRow =
37 | { kind: 'blank' }
38 | { kind: 'heading'; text: string }
39 | { kind: 'raw'; text: string }
40 | { kind: 'note'; text: string }
41 | { kind: 'table'; label: string; value: string }
42 | { kind: 'text'; text: string };
43
44function outputRow(line: string): OutputRow {
45 if (!line) return { kind: 'blank' };
46 // Stats reports (src/optim_jev/core/report.py): titles, box-drawn tables, notes.
47 if (line.startsWith('▸ ')) return { kind: 'heading', text: line.slice(2) };
48 if (/^[┌│├└]/.test(line)) return { kind: 'raw', text: line };
49 if (line.startsWith('- ')) return { kind: 'note', text: line.slice(2) };
50 const cost = /^\s{2,}(.+?)\s{2,}(\$[\d,.]+.*)$/.exec(line);
51 if (cost) return { kind: 'table', label: cost[1]!.trim(), value: cost[2]!.trim() };
52 const colon = line.indexOf(':');
53 if (!line.startsWith('-') && colon > 0 && colon < 40 && colon < line.length - 1) {
54 return { kind: 'table', label: line.slice(0, colon).trim(), value: line.slice(colon + 1).trim() };
55 }
56 return { kind: 'text', text: line };
57}
58
59export function commandOutputTitle(command: string, args: string): string | null {
60 const verb = args.trim().split(/\s+/, 1)[0] ?? '';
61 return TITLES[command]?.[verb] ?? null;
62}
63
64export function commandOutputTree(
65 elements: Elements,
66 command: string,
67 args: string,
68 output: string,
69 isErrored = false,
70 columns = 100,
71) {
72 const title = commandOutputTitle(command, args);
73 if (!title) return null;
74 const { Box, Text } = elements;
75 const rows = output.split('\n').map(outputRow);
76 const widestLabel = Math.max(0, ...rows.map((row) => row.kind === 'table' ? row.label.length : 0));
77 const labelWidth = Math.max(12, Math.min(28, widestLabel + 2, Math.floor(columns * 0.36)));
78
79 return (
80 <Box key="optim-jev-command-output" flexDirection="column">
81 <Text>
82 ╭─ ◆ <Text bold>{title}</Text>
83 </Text>
84 {rows.map((row, index) => (
85 <Box key={`output-${index}`} flexDirection="row">
86 <Text>│{row.kind === 'blank' ? '' : ' '}</Text>
87 {row.kind === 'table' ? (
88 <Box flexDirection="row" flexGrow={1} minWidth={0}>
89 <Box width={labelWidth} flexShrink={0}>
90 <Text bold>
91 {row.label}
92 </Text>
93 </Box>
94 <Box flexGrow={1} flexShrink={1} minWidth={0}>
95 {outputText(Text, row.value, isErrored)}
96 </Box>
97 </Box>
98 ) : row.kind === 'heading' ? (
99 <Text bold>{row.text}</Text>
100 ) : row.kind === 'raw' ? (
101 <Text wrap="truncate-end">{valueParts(Text, row.text)}</Text>
102 ) : row.kind === 'note' ? (
103 <Text wrap="wrap" dimColor>
104 {row.text}
105 </Text>
106 ) : row.kind === 'text' ? outputText(Text, row.text, isErrored) : null}
107 </Box>
108 ))}
109 <Text>╰─</Text>
110 </Box>
111 );
112}
113hooks/router-lib.ts 149 lines1import type { PluginOptions } from 'claude-code';
2
3export const ROUTER_MODULE = 'optim_jev.tools.jev_router.tool';
4export const ROUTER_COMMAND = 'jev-route';
5export const SECTION_ID = 'optim-jev:jev-router';
6export const STORE_KEY = 'jev_router_session';
7export const MODES = ['off', 'on'] as const;
8export const DEFAULT_ESCALATE_AFTER = 3;
9export const DEFAULT_INIT_MODEL = 'sonnet';
10/** Model call errors that mean the account can't use that model (not a transient failure). */
11export const MODEL_GONE = new Set<string>(['model_not_found', 'invalid_request', 'oauth_org_not_allowed']);
12export const DELETE_COMMANDS = new Set(['rm', 'rmdir', 'del', 'erase', 'unlink', 'remove-item', 'ri', 'mv', 'move', 'move-item']);
13
14/** The routing files Claude may edit itself while routing is on (project-relative). */
15export const RULES_FILES = ['.optim-jev/jev_router/rules.json', '.optim-jev/jev_router/RULES.md'];
16export const isRulesFile = (rel: string) => RULES_FILES.some((f) => f.toLowerCase() === rel.toLowerCase());
17
18export type Mode = (typeof MODES)[number];
19export type Permission = { write: string[]; delete: string[] };
20export type RenderAnswer = {
21 section: string;
22 rules: Record<string, Permission>;
23 tiers: string[];
24 current: string | null;
25 has_rules: boolean;
26};
27export type SpawnAnswer = {
28 model: string;
29 effort: Effort | null;
30 files: string[];
31 appendix: string;
32 toast: string;
33 source: 'jev' | 'rules' | 'escalation' | 'claude' | 'fallback';
34 escalated: number;
35 jev_failed: string | null;
36};
37/** Jev's input was over budget even after trimming: Claude is asked instead, with this prompt. */
38export type SpawnAskClaude = { ask_claude: string; reason: string | null };
39export type Effort = 'low' | 'medium' | 'high' | 'xhigh';
40export const EFFORTS: readonly Effort[] = ['low', 'medium', 'high', 'xhigh'];
41/** Per file: model levels to move up, and the effort to keep, after errors there. */
42export type Escalated = { levels: number; effort: Effort | null };
43export const MAX_RECENT_INPUTS = 4;
44const RECENT_INPUT_STORE_CHARS = 2000;
45
46/**
47 * The person's own prompts, newest last, at most MAX_RECENT_INPUTS: no slash commands, nothing the
48 * plugin or Claude Code injected (system reminders, our shortlist notes).
49 */
50export function rememberInput(recent: string[], text: string): string[] {
51 const t = text.trim();
52 if (!t || t.startsWith('/') || t.startsWith('<') || t.startsWith('optim-jev')) return recent;
53 return [...recent, t.slice(0, RECENT_INPUT_STORE_CHARS)].slice(-MAX_RECENT_INPUTS);
54}
55
56/** Claude's routing answer: `{"model": "...", "effort": "..."}`, possibly wrapped in prose or a fence. */
57export function parseDecision(text: string): { model?: string; effort?: string } | null {
58 const match = /\{[^{}]*\}/.exec(text);
59 if (!match) return null;
60 try {
61 const value = JSON.parse(match[0]) as Record<string, unknown>;
62 return {
63 ...(typeof value['model'] === 'string' ? { model: value['model'].toLowerCase() } : {}),
64 ...(typeof value['effort'] === 'string' ? { effort: value['effort'].toLowerCase() } : {}),
65 };
66 } catch {
67 return null;
68 }
69}
70export type SessionRecord = { sessionId: string; mode: Mode };
71export type Escalation = { from: string; to: string; effort: Effort | null; files: string[] };
72export type TurnState = {
73 spawned: number;
74 errors: Map<string, number>;
75 escalations: Escalation[];
76 deniedCount: number;
77};
78
79export const USAGE = [
80 'Usage: /jev-route [on | off]',
81 ' /jev-route init [--depth N] [--force] ["what matters in this project"]',
82 ' /jev-route why | stats',
83].join('\n');
84
85export function newTurn(): TurnState {
86 return { spawned: 0, errors: new Map(), escalations: [], deniedCount: 0 };
87}
88
89export function oneOf<T extends string>(values: readonly T[], value: unknown, fallback: T): T {
90 return typeof value === 'string' && (values as readonly string[]).includes(value) ? (value as T) : fallback;
91}
92
93export function numberOption(options: PluginOptions, key: string, fallback: number): number {
94 const raw = options[key];
95 const value = typeof raw === 'string' ? Number(raw) : raw;
96 return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
97}
98
99export function relPath(file: string, root: string): string {
100 const norm = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, '');
101 let path = norm(file);
102 const base = norm(root);
103 if (path.toLowerCase().startsWith(base.toLowerCase() + '/')) path = path.slice(base.length + 1);
104 while (path.startsWith('./')) path = path.slice(2);
105 return path;
106}
107
108export function lookup(rules: Record<string, Permission>, rel: string, tiers: string[]): Permission {
109 const index = new Map(Object.entries(rules).map(([k, v]) => [k.toLowerCase(), v]));
110 let probe = rel.toLowerCase().replace(/\/+$/, '');
111 const exact = index.get(probe);
112 if (exact) return exact;
113 while (probe) {
114 const hit = index.get(probe + '/') ?? index.get(probe);
115 if (hit) return hit;
116 probe = probe.includes('/') ? probe.slice(0, probe.lastIndexOf('/')) : '';
117 }
118 return { write: tiers, delete: tiers };
119}
120
121export function words(segment: string): string[] {
122 return [...segment.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? '');
123}
124
125/** Paths a shell command deletes or moves away; best effort, not a security boundary. */
126export function deletedPaths(command: string): string[] {
127 const paths: string[] = [];
128 for (const segment of command.split(/&&|\|\||[;|\n]/)) {
129 let argv = words(segment.trim());
130 if (argv[0] === 'git' && (argv[1] === 'rm' || argv[1] === 'mv')) argv = argv.slice(1);
131 if (argv[0] === 'sudo') argv = argv.slice(1);
132 const name = (argv[0] ?? '').toLowerCase();
133 if (!DELETE_COMMANDS.has(name)) continue;
134 const args = argv.slice(1).filter((a) => !a.startsWith('-') && !(a.startsWith('/') && a.length <= 3));
135 const isMove = ['mv', 'move', 'move-item'].includes(name);
136 paths.push(...(isMove ? args.slice(0, -1) : args));
137 }
138 return paths.filter(Boolean);
139}
140
141export function writeTargets(e: Record<string, unknown>): { paths: string[]; kind: 'write' | 'delete' } {
142 const tool = String(e['tool']);
143 if (tool === 'Edit' || tool === 'Write') return { paths: [String(e['file_path'] ?? '')], kind: 'write' };
144 if (tool === 'NotebookEdit') return { paths: [String(e['notebook_path'] ?? '')], kind: 'write' };
145 if (tool === 'Bash' || tool === 'PowerShell') return { paths: deletedPaths(String(e['command'] ?? '')), kind: 'delete' };
146 return { paths: [], kind: 'write' };
147}
148
149