Six Claude Code mods: five under one band above the prompt, waypoints (the next steps, one press each), resin (cache, context, limits and cost, with warm…

[![Apache-2.0 licensed][badge-license]][url-license] [![NPM version][badge-npm-version]][url-npm] [![NPM downloads][badge-npm-downloads]][url-npm] [![NPM Unpacked Size (with version)][badge-npm-unpacked-size]][url-npm]
A Claude Code plugin of six mods: five drawn as one band above the prompt, in the session character's colour (the next steps one press away, what the session is spending with a one-press handoff, recording mode, a goal's task list and clock, and a question before editing a file another session just changed), and a delegation guard that nudges a run of lookups toward a haiku agent.
This repository is a Claude Code plugin marketplace named esposter:
claude plugin marketplace add Esposter/Esposter
claude plugin install genshin-mods@esposter
A clone of this repository needs neither command: .agents/settings.json enables the plugin at project scope once you trust the repository. With genshin-persona installed beside it, the band takes the session character's colour; without it, the game's interface gold.
Developing the plugin from a checkout loads the directory itself, with the installed copy disabled so only one of them answers:
claude plugin disable genshin-mods@esposter
claude --plugin-dir packages/genshin-mods
We highly recommend you take a look at the documentation to level up.
Each mod is switched by its own slash command; bare, it flips, and the choice is kept across sessions:
| Command | What it does | |
|---|---|---|
| `/waypoints [on \ | off]` | Suggests the next steps after each answered turn, or stops |
| `/resin [on \ | off]` | Shows the cache, context, limits and cost row and its warning toast |
| `/veil [on \ | off]` | Turns recording mode on or off; off by default |
| `/commission [on \ | off]` | Shows the goal meter while a turn works through a task list |
| `/ward [on \ | off]` | Asks before an edit to a file another session just changed |
With the band focused (a click or ctrl+x tab), w, c and h warm, compact and hand off, 1 to 3 send a waypoint and g opens or closes the whole task list.
Run from packages/genshin-mods/:
pnpm test # vitest watch mode (coverage is run from the repo root)
pnpm validate # claude plugin validate: the engine's own reading of the hooks module
pnpm lint:fix # auto-fix lint
This project is licensed under the Apache-2.0 license.
[badge-license]: https://img.shields.io/github/license/Esposter/Esposter.svg?color=blue [url-license]: https://github.com/Esposter/Esposter/blob/main/LICENSE [badge-npm-version]: https://img.shields.io/npm/v/genshin-mods/latest?color=brightgreen [url-npm]: https://www.npmjs.com/package/genshin-mods/v/latest [badge-npm-unpacked-size]: https://img.shields.io/npm/unpacked-size/genshin-mods/latest?label=npm [badge-npm-downloads]: https://img.shields.io/npm/dm/genshin-mods.svg
src/register.ts 18 lines1import type { Register } from "claude-code";
2
3import { registerBand } from "./services/band/registerBand";
4import { registerCommission } from "./services/commission/registerCommission";
5import { registerDelegation } from "./services/delegation/registerDelegation";
6import { registerLifecycle } from "./services/registerLifecycle";
7import { registerVeil } from "./services/veil/registerVeil";
8import { registerWard } from "./services/ward/registerWard";
9
10export const register: Register = (on) => {
11 registerBand(on);
12 registerCommission(on);
13 registerDelegation(on);
14 registerLifecycle(on);
15 registerVeil(on);
16 registerWard(on);
17};
18src/services/band/registerBand.ts 216 lines1import type { EngineInterface, On, RenderElement, RenderInput } from "claude-code";
2
3import { atom, read, update } from "claude-code";
4
5import type { EnabledMods } from "../../../types";
6
7import { ACCENT_COLOR, HANDOFF_QUESTION, MAX_SHOWN_TASKS, WARM_QUESTION } from "../constants";
8import { InitialState } from "../InitialState";
9import { getResinFigures } from "../resin/getResinFigures";
10import { getCommissionSummary } from "./getCommissionSummary";
11
12// The plugin's one band, a row per mod with something to say. `$` is followed only into functions in the file that
13// Hooked it, so each row and each button's action lives here beside the hook
14const commissionAtom = atom({ key: "commission", plugin: "genshin-mods" } as const, InitialState.commission);
15const enabledModsAtom = atom({ key: "enabledMods", plugin: "genshin-mods" } as const, InitialState.enabledMods);
16const isCommissionExpandedAtom = atom(
17 { key: "isCommissionExpanded", plugin: "genshin-mods" } as const,
18 InitialState.isCommissionExpanded,
19);
20const isHandingOffAtom = atom({ key: "isHandingOff", plugin: "genshin-mods" } as const, InitialState.isHandingOff);
21const lastCacheRequestAtAtom = atom(
22 { key: "lastCacheRequestAt", plugin: "genshin-mods" } as const,
23 InitialState.lastCacheRequestAt,
24);
25const nowAtom = atom({ key: "now", plugin: "genshin-mods" } as const, InitialState.now);
26const waypointsAtom = atom({ key: "waypoints", plugin: "genshin-mods" } as const, InitialState.waypoints);
27
28// The engine's press slot takes no promise; every action here resolves each of its outcomes itself
29const press = (action: () => Promise<unknown>) => () => {
30 // oxlint-disable-next-line typescript/no-floating-promises -- The press slot is the engine's and returns nothing
31 action();
32};
33
34const StatusMarkMap = { completed: "✓", in_progress: "▸", pending: "·" } as const;
35
36// A fork re-sends the conversation's prefix, which renews the cache from the moment it is sent, and adds no row to the
37// Transcript
38const warmCache = async ($: EngineInterface) => {
39 const requestedAt = await $.clock.now();
40 const answer = await $.model.fork({ prompt: WARM_QUESTION });
41 if (!answer.isAnswered) {
42 $.ui.toast(`The cache was not warmed: ${answer.reason}.`);
43 return;
44 }
45
46 await update($, lastCacheRequestAtAtom, () => requestedAt);
47 await update($, nowAtom, () => requestedAt);
48};
49
50// The whole relay in one press: the clear waits for the handoff text, so a fork that fails clears nothing
51const relay = async ($: EngineInterface) => {
52 const answer = await $.model.fork({ prompt: HANDOFF_QUESTION });
53 if (answer.isAnswered && answer.text.trim()) {
54 await $.command.run({ command: "clear" });
55 await $.prompt.submit({ text: answer.text });
56 } else $.ui.toast(`The handoff was not written: ${answer.isAnswered ? "it came back empty" : answer.reason}.`);
57};
58
59// The relay is settled rather than awaited, so a clear or submit that rejects still brings the buttons back
60const handOff = async ($: EngineInterface) => {
61 await update($, isHandingOffAtom, () => true);
62 const [outcome] = await Promise.allSettled([relay($)]);
63 await update($, isHandingOffAtom, () => false);
64 if (outcome.status === "rejected") $.ui.toast(`The handoff failed: ${String(outcome.reason)}.`);
65};
66
67// The session character's colour, which the persona publishes, else the game's interface gold
68const readAccent = async ($: EngineInterface) =>
69 (await read($, { key: "character", plugin: "genshin-persona" } as const))?.color || ACCENT_COLOR;
70
71const drawResinRow = async (
72 $: EngineInterface,
73 e: RenderInput<"AbovePrompt">,
74 enabledMods: EnabledMods,
75 accent: string,
76): Promise<RenderElement | undefined> => {
77 const lastCacheRequestAt = await read($, lastCacheRequestAtAtom);
78 if (lastCacheRequestAt === 0 || !enabledMods.resin) return undefined;
79
80 const figures = getResinFigures(await $.session.usage(), lastCacheRequestAt, await read($, nowAtom));
81 const isHandingOff = await read($, isHandingOffAtom);
82 const { Box, Button, Text } = $.ui.resolve(e);
83 return Box({
84 children: [
85 Text({ bold: true, children: "Resin ", color: accent }),
86 ...figures.map(({ isWarning, label, text }) =>
87 Text({ children: `${label} ${text} `, color: isWarning ? "yellow" : undefined, dimColor: !isWarning }),
88 ),
89 isHandingOff
90 ? Text({ children: "writing the handoff…", dimColor: true })
91 : Box({
92 children: [
93 Button({ hotkey: "w", key: "resin-warm", label: "Warm", onPress: press(() => warmCache($)) }),
94 Button({
95 hotkey: "c",
96 key: "resin-compact",
97 label: "Compact",
98 onPress: press(() => $.session.compact()),
99 }),
100 Button({
101 hotkey: "h",
102 key: "resin-handoff",
103 label: "Handoff",
104 onPress: press(() => handOff($)),
105 variant: "primary",
106 }),
107 ],
108 flexDirection: "row",
109 }),
110 ],
111 flexDirection: "row",
112 flexWrap: "wrap",
113 });
114};
115
116const drawCommissionRow = async (
117 $: EngineInterface,
118 e: RenderInput<"AbovePrompt">,
119 enabledMods: EnabledMods,
120 accent: string,
121): Promise<RenderElement | undefined> => {
122 const commission = await read($, commissionAtom);
123 if (commission.tasks.length === 0 || !enabledMods.commission) return undefined;
124
125 const isExpanded = await read($, isCommissionExpandedAtom);
126 const shownTasks = isExpanded
127 ? commission.tasks.slice(0, MAX_SHOWN_TASKS)
128 : commission.tasks.filter(({ status }) => status === "in_progress");
129 const { Box, Button, Text } = $.ui.resolve(e);
130 return Box({
131 children: [
132 Box({
133 children: [
134 Text({ bold: true, children: "Commission ", color: accent }),
135 Text({ children: `${getCommissionSummary(commission, await read($, nowAtom))} `, wrap: "truncate-end" }),
136 Button({
137 hotkey: "g",
138 key: "commission-expand",
139 label: isExpanded ? "Fewer" : "All tasks",
140 onPress: press(() => update($, isCommissionExpandedAtom, (value) => !value)),
141 plain: true,
142 }),
143 ],
144 flexDirection: "row",
145 }),
146 ...shownTasks.map(({ status, subject }) =>
147 Text({
148 children: ` ${StatusMarkMap[status]} ${subject}`,
149 dimColor: status !== "in_progress",
150 wrap: "truncate-end",
151 }),
152 ),
153 ],
154 flexDirection: "column",
155 });
156};
157
158const drawWaypointsRow = async (
159 $: EngineInterface,
160 e: RenderInput<"AbovePrompt">,
161 enabledMods: EnabledMods,
162 accent: string,
163): Promise<RenderElement | undefined> => {
164 const waypoints = await read($, waypointsAtom);
165 if (waypoints.length === 0 || e.props.isWorking || !enabledMods.waypoints) return undefined;
166
167 const { Box, Button, Text } = $.ui.resolve(e);
168 return Box({
169 children: [
170 Text({ bold: true, children: "Waypoints", color: accent }),
171 ...waypoints.map((waypoint, index) =>
172 Button({
173 hotkey: `${index + 1}`,
174 key: `waypoint-${index}`,
175 label: waypoint,
176 onPress: press(() => $.prompt.submit({ text: waypoint })),
177 plain: true,
178 }),
179 ),
180 Button({
181 key: "waypoints-dismiss",
182 label: "Dismiss",
183 onPress: press(() => update($, waypointsAtom, () => InitialState.waypoints)),
184 plain: true,
185 role: "dismiss",
186 }),
187 ],
188 flexDirection: "column",
189 });
190};
191
192// The veil's marker first, so a recording never runs with it off unnoticed, and the engine's own band whenever no
193// Mod has anything to say
194export const registerBand = (on: On): void => {
195 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
196 if (e.props.hasSurvey) return next(e);
197
198 const enabledMods = await read($, enabledModsAtom);
199 const accent = await readAccent($);
200 const rows = (
201 await Promise.all([
202 drawResinRow($, e, enabledMods, accent),
203 drawCommissionRow($, e, enabledMods, accent),
204 drawWaypointsRow($, e, enabledMods, accent),
205 ])
206 ).filter((row) => row !== undefined);
207 if (rows.length === 0 && !enabledMods.veil) return next(e);
208
209 const { Box, Text } = $.ui.resolve(e);
210 const marker = enabledMods.veil
211 ? [Text({ bold: true, children: "● Veil on: values are hidden on screen", color: "red" })]
212 : [];
213 return Box({ children: [...marker, ...rows], flexDirection: "column" });
214 });
215};
216src/services/commission/registerCommission.ts 49 lines1import type { EngineInterface, On } from "claude-code";
2
3import { atom, read, update } from "claude-code";
4
5import type { TaskCall } from "../../models/TaskCall";
6
7import { TaskTool } from "../../models/TaskTool";
8import { InitialState } from "../InitialState";
9import { foldTaskCall } from "./foldTaskCall";
10
11const commissionAtom = atom({ key: "commission", plugin: "genshin-mods" } as const, InitialState.commission);
12const enabledModsAtom = atom({ key: "enabledMods", plugin: "genshin-mods" } as const, InitialState.enabledMods);
13const lastPromptAtom = atom({ key: "lastPrompt", plugin: "genshin-mods" } as const, InitialState.lastPrompt);
14
15// A task tool's call folded in once the tool has answered, opening the commission on its first task with the goal
16// Read off the prompt that started the turn
17const fold = async ($: EngineInterface, call: TaskCall) => {
18 if (!(await read($, enabledModsAtom)).commission) return;
19 const now = await $.clock.now();
20 const lastPrompt = await read($, lastPromptAtom);
21 await update($, commissionAtom, (commission) => ({
22 goal: commission.goal || (lastPrompt.split("\n").find(Boolean) ?? "").trim(),
23 openedAt: commission.openedAt || now,
24 tasks: foldTaskCall(commission.tasks, call),
25 }));
26};
27
28export const registerCommission = (on: On): void => {
29 on("tool.call", { tool: "TaskCreate" }, async ($, e, next) => {
30 const result = await next(e);
31 if (!result.deny && !result.isError && result.result)
32 await fold($, { id: result.result.task.id, subject: e.subject, tool: TaskTool.TaskCreate });
33 return result;
34 });
35
36 on("tool.call", { tool: "TaskUpdate" }, async ($, e, next) => {
37 const result = await next(e);
38 if (!result.deny && !result.isError)
39 await fold($, { id: e.taskId, status: e.status, subject: e.subject, tool: TaskTool.TaskUpdate });
40 return result;
41 });
42
43 on("tool.call", { tool: "TodoWrite" }, async ($, e, next) => {
44 const result = await next(e);
45 if (!result.deny && !result.isError) await fold($, { todos: e.todos, tool: TaskTool.TodoWrite });
46 return result;
47 });
48};
49src/services/delegation/registerDelegation.ts 43 lines1import type { EngineInterface, On } from "claude-code";
2
3import { atom, update } from "claude-code";
4
5import type { DelegationCall } from "../../models/DelegationCall";
6
7import { DelegationStep } from "../../models/DelegationStep";
8import { InitialState } from "../InitialState";
9import { getCallStep } from "./getCallStep";
10import { getNextStreak } from "./getNextStreak";
11import { getNudge } from "./getNudge";
12
13const lookupStreakAtom = atom({ key: "lookupStreak", plugin: "genshin-mods" } as const, InitialState.lookupStreak);
14
15// The count is one update, so calls the model sends in one batch each see the count the call before them left. The
16// Nudge is the count's, and a neutral call earns none
17const countCall = async ($: EngineInterface, call: DelegationCall): Promise<string | undefined> => {
18 const step = getCallStep(call);
19 if (step === DelegationStep.Neutral) return undefined;
20 const streak = await update($, lookupStreakAtom, (value) => getNextStreak(value, step));
21 return step === DelegationStep.Lookup ? getNudge(streak) : undefined;
22};
23
24// Any failure lets the call through, as the ward's does: a count that blocked on its own failure would block every
25// Call after it. `next` is replay-safe here, so a call the hook had already counted is not run twice
26const letCallThrough = <E, R>(_$: EngineInterface, e: E, next: (e: E) => R): R => next(e);
27
28export const registerDelegation = (on: On): void => {
29 // A nudge rides on the call's own result as context the model reads after it, and never blocks the call
30 // eslint-disable-next-line no-restricted-syntax -- The engine's registration handler, run when the hook rejects, not a promise
31 on(
32 "tool.call",
33 { tool: ["Agent", "Bash", "Edit", "Glob", "Grep", "NotebookEdit", "PowerShell", "Read", "WebFetch", "Write"] },
34 async ($, e, next) => {
35 const nudge = await countCall($, e);
36 const result = await next(e);
37 return nudge !== undefined && result.deny === undefined
38 ? { ...result, context: [...(result.context ?? []), nudge] }
39 : result;
40 },
41 ).catch(letCallThrough);
42};
43src/services/registerLifecycle.ts 192 lines1import type { EngineInterface, On } from "claude-code";
2
3import { atom, read, update } from "claude-code";
4
5import type { EnabledMods } from "../../types";
6
7import {
8 CACHE_WARNING_MS,
9 CLOCK_TICK_MS,
10 RESERVE_SECTION_ID,
11 USAGE_RESERVE_PERCENTAGE,
12 VEIL_SECTION_ID,
13 VEIL_SYSTEM_SECTION,
14 WAYPOINTS_QUESTION,
15} from "./constants";
16import { InitialState } from "./InitialState";
17import { ModDescriptionMap, ModNames } from "./ModDescriptionMap";
18import { formatResetsAt } from "./resin/formatResetsAt";
19import { getCacheRemainingMs } from "./resin/getCacheRemainingMs";
20import { getReserveWindow } from "./resin/getReserveWindow";
21import { reserveText } from "./resin/reserveText";
22import { parseWaypoints } from "./waypoints/parseWaypoints";
23
24const commissionAtom = atom({ key: "commission", plugin: "genshin-mods" } as const, InitialState.commission);
25const enabledModsAtom = atom({ key: "enabledMods", plugin: "genshin-mods" } as const, InitialState.enabledMods);
26const isCommissionExpandedAtom = atom(
27 { key: "isCommissionExpanded", plugin: "genshin-mods" } as const,
28 InitialState.isCommissionExpanded,
29);
30const lastPromptAtom = atom({ key: "lastPrompt", plugin: "genshin-mods" } as const, InitialState.lastPrompt);
31const lastCacheRequestAtAtom = atom(
32 { key: "lastCacheRequestAt", plugin: "genshin-mods" } as const,
33 InitialState.lastCacheRequestAt,
34);
35const lookupStreakAtom = atom({ key: "lookupStreak", plugin: "genshin-mods" } as const, InitialState.lookupStreak);
36const nowAtom = atom({ key: "now", plugin: "genshin-mods" } as const, InitialState.now);
37const reserveWindowAtom = atom({ key: "reserveWindow", plugin: "genshin-mods" } as const, InitialState.reserveWindow);
38const waypointsAtom = atom({ key: "waypoints", plugin: "genshin-mods" } as const, InitialState.waypoints);
39
40// The events a plugin may hook once with no matcher, each hooked here for every mod, with the commands that switch
41// The mods. Neither variable is drawn: the expiry already warned about, so each warns once, and whether a turn is
42// Renewing the cache itself, when no warning is owed
43let warnedLastCacheRequestAt = 0;
44let isTurnRunning = false;
45
46const tick = async ($: EngineInterface) => {
47 const now = await $.clock.now();
48 await update($, nowAtom, () => now);
49 const lastCacheRequestAt = await read($, lastCacheRequestAtAtom);
50 const remainingMs = getCacheRemainingMs(lastCacheRequestAt, now);
51 const isWarningDue = lastCacheRequestAt > 0 && remainingMs > 0 && remainingMs <= CACHE_WARNING_MS;
52 if (
53 !isWarningDue ||
54 isTurnRunning ||
55 warnedLastCacheRequestAt === lastCacheRequestAt ||
56 !(await read($, enabledModsAtom)).resin
57 )
58 return;
59
60 warnedLastCacheRequestAt = lastCacheRequestAt;
61 $.ui.toast("The prompt cache goes cold in five minutes: warm it, compact or hand off from the band.");
62};
63
64const suggestWaypoints = async ($: EngineInterface) => {
65 const answer = await $.model.fork({ prompt: WAYPOINTS_QUESTION });
66 if (answer.isAnswered) await update($, waypointsAtom, () => parseWaypoints(answer.text));
67};
68
69const closeCommission = async ($: EngineInterface) => {
70 await update($, commissionAtom, () => InitialState.commission);
71 await update($, isCommissionExpandedAtom, () => InitialState.isCommissionExpanded);
72};
73
74export const registerLifecycle = (on: On): void => {
75 // The bare command flips a mod and an argument sets it, kept in the store so it holds for every later session
76 for (const name of ModNames)
77 on("command.run", { command: name }, async ($, e) => {
78 const argument = e.args.trim().toLowerCase();
79 const enabledMods = await update($, enabledModsAtom, (value) => ({
80 ...value,
81 [name]: argument ? argument === "on" : !value[name],
82 }));
83 await $.store.set(enabledModsAtom.ref.key, enabledMods);
84 return { text: `${name} is ${enabledMods[name] ? "on" : "off"}.` };
85 });
86
87 on("session.start", async ($, e, next) => {
88 const stored = (await $.store.get(enabledModsAtom.ref.key)) as Partial<EnabledMods> | undefined;
89 await update($, enabledModsAtom, (value) => ({ ...value, ...stored }));
90 await Promise.all(
91 ModNames.map((name) =>
92 $.command.register({ argumentHint: "[on|off]", description: ModDescriptionMap[name], name }),
93 ),
94 );
95 // oxlint-disable-next-line unicorn/no-array-method-this-argument -- The engine's clock, not Array.prototype.every
96 $.clock.every(CLOCK_TICK_MS, () => {
97 // oxlint-disable-next-line typescript/no-floating-promises -- The engine's timer slot takes no promise, and every call in the tick resolves
98 tick($);
99 });
100 return next(e);
101 });
102
103 // A `/clear` or a resume carries on in this process under another conversation, with no `session.start` for it, so
104 // Nothing the old one showed is kept
105 on("session.end", async ($, e, next) => {
106 if (e.reason === "clear" || e.reason === "resume") {
107 await update($, lastCacheRequestAtAtom, () => InitialState.lastCacheRequestAt);
108 await update($, waypointsAtom, () => InitialState.waypoints);
109 await closeCommission($);
110 }
111
112 return next(e);
113 });
114
115 // The prompt is kept as the goal of a commission this turn may open, and a commission whose every task is done has
116 // Shown its finished row since the last turn ended, so it closes as the person moves on. A new prompt also starts
117 // The lookup count again, since the lookups of the last turn are not the chain of this one
118 on("prompt.submit", async ($, e, next) => {
119 await update($, waypointsAtom, () => InitialState.waypoints);
120 await update($, lookupStreakAtom, () => InitialState.lookupStreak);
121 await update($, lastPromptAtom, () => e.text);
122 const { tasks } = await read($, commissionAtom);
123 if (tasks.length > 0 && tasks.every(({ status }) => status === "completed")) await closeCommission($);
124 return next(e);
125 });
126
127 // While the veil is on, the model is told to write placeholders too, so a reply never holds a value to hide. While a
128 // Usage reserve holds and the resin mod is on, the model is told to wind down until the window resets, which it may
129 // Have done since the last measurement
130 on("prompt.compose", async ($, e, next) => {
131 const result = await next(e);
132 const { resin, veil } = await read($, enabledModsAtom);
133 const reserveWindow = await read($, reserveWindowAtom);
134 const sections = [...result.sections];
135 if (veil) sections.push({ id: VEIL_SECTION_ID, scope: "session", text: VEIL_SYSTEM_SECTION });
136 if (resin && reserveWindow.name && Date.parse(reserveWindow.resetsAt) > (await $.clock.now()))
137 sections.push({ id: RESERVE_SECTION_ID, scope: "session", text: reserveText(reserveWindow) });
138 return { sections };
139 });
140
141 on("turn.start", (_$, e, next) => {
142 isTurnRunning = true;
143 return next(e);
144 });
145
146 // The cache's lifetime runs from the request that read or wrote it, so each of the main conversation's requests
147 // Restarts the countdown as it is sent, not as its reply ends
148 on("turn.step", async function* ($, e, next) {
149 if (e.agentId === undefined) {
150 const now = await $.clock.now();
151 await update($, lastCacheRequestAtAtom, () => now);
152 await update($, nowAtom, () => now);
153 }
154
155 return yield* next(e);
156 });
157
158 // An answer of the main conversation asks for waypoints off the turn's own dispatch, so the fork never holds the
159 // Turn's end or the next prompt behind it
160 on("turn.complete", async ($, e, next) => {
161 const result = await next(e);
162 if (e.agentId !== undefined) return result;
163
164 isTurnRunning = false;
165
166 const now = await $.clock.now();
167 await update($, nowAtom, () => now);
168 if (e.reason === "answer" && (await read($, enabledModsAtom)).waypoints)
169 $.clock.after(0, () => {
170 // oxlint-disable-next-line typescript/no-floating-promises -- The engine's timer slot takes no promise, and a fork resolves every outcome rather than rejecting
171 suggestWaypoints($);
172 });
173 return result;
174 });
175
176 // The reserve is read off each measurement: it starts at the first window past the line and lifts at the measurement
177 // A window's reset raises, which finds none. The toast names the window once, as the reserve starts
178 on("session.measure", async ($, e, next) => {
179 const reserveWindow = getReserveWindow(e.rateLimits);
180 const previousReserveWindow = await read($, reserveWindowAtom);
181 await update($, reserveWindowAtom, () => reserveWindow ?? InitialState.reserveWindow);
182 if (reserveWindow && !previousReserveWindow.name && (await read($, enabledModsAtom)).resin) {
183 const { name, resetsAt } = reserveWindow;
184 $.ui.toast(
185 `Usage reserve: the ${name} usage window has passed ${USAGE_RESERVE_PERCENTAGE}% and resets at ${formatResetsAt(resetsAt)}.`,
186 );
187 }
188
189 return next(e);
190 });
191};
192src/services/veil/registerVeil.ts 33 lines1import type { On } from "claude-code";
2
3import { atom, read } from "claude-code";
4
5import { InitialState } from "../InitialState";
6import { veilText } from "./veilText";
7import { veilValue } from "./veilValue";
8
9const enabledModsAtom = atom({ key: "enabledMods", plugin: "genshin-mods" } as const, InitialState.enabledMods);
10
11// Only the drawing changes: the transcript and what the model reads keep the real values. Each read subscribes the
12// Row, so switching the veil redraws every row already on screen. The instruction the veil adds to the system prompt is
13// An unmatched hook, so it is the lifecycle file's
14export const registerVeil = (on: On): void => {
15 on("ui.render", { component: "AssistantMessage" }, async ($, e, next) =>
16 (await read($, enabledModsAtom)).veil
17 ? next({ ...e, props: { ...e.props, text: veilText(e.props.text) } })
18 : next(e),
19 );
20
21 on("ui.render", { component: "UserMessage" }, async ($, e, next) =>
22 (await read($, enabledModsAtom)).veil
23 ? next({ ...e, props: { ...e.props, text: veilText(e.props.text) } })
24 : next(e),
25 );
26
27 on("ui.render", { component: "ToolResult" }, async ($, e, next) =>
28 (await read($, enabledModsAtom)).veil
29 ? next({ ...e, props: { ...e.props, output: veilValue(e.props.output) } })
30 : next(e),
31 );
32};
33src/services/ward/registerWard.ts 83 lines1import type { EngineInterface, On, ToolCallResult } from "claude-code";
2
3import { atom, read } from "claude-code";
4
5import type { WardRecord } from "../../../types";
6
7import { WardAnswer, WardAnswers } from "../../models/WardAnswer";
8import { InitialState } from "../InitialState";
9import { getCollidingRecord } from "./getCollidingRecord";
10import { getRecordsWithEdit } from "./getRecordsWithEdit";
11
12const enabledModsAtom = atom({ key: "enabledMods", plugin: "genshin-mods" } as const, InitialState.enabledMods);
13const AGE_FORMATTER = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
14
15// One file every session on the machine reads afresh, beside the persona's state, so an edit made a moment ago in
16// Another session is always seen
17const readRecordsPath = async ($: EngineInterface) => {
18 const home = (await $.env.get("HOME")) ?? (await $.env.get("USERPROFILE")) ?? "";
19 return `${home}/.claude/genshin-mods/ward.json`;
20};
21
22const readRecords = async ($: EngineInterface, path: string): Promise<Record<string, WardRecord>> =>
23 (await $.fs.exists(path))
24 ? // oxlint-disable-next-line no-restricted-properties -- The records hold numbers and ids, no date, and a mod cannot import the shared reviver
25 (JSON.parse(await $.fs.read(path)) as Record<string, WardRecord>)
26 : {};
27
28// Before the edit: the question, when another session's record calls for one. After it: the edit recorded
29const ward = async (
30 $: EngineInterface,
31 path: string,
32 proceed: () => Promise<ToolCallResult>,
33): Promise<ToolCallResult> => {
34 if (!(await read($, enabledModsAtom)).ward) return proceed();
35 const recordsPath = await readRecordsPath($);
36 const sessionId = await $.session.id();
37 const now = await $.clock.now();
38 const collision = getCollidingRecord((await readRecords($, recordsPath))[path], sessionId, now);
39 // A session nobody watches has nobody to ask, and a guard that blocks unattended work is worse than none
40 if (collision && (await $.session.surfaces()).length > 0) {
41 const minutes = -Math.round(Temporal.Duration.from({ milliseconds: now - collision.editedAt }).total("minutes"));
42 const age = AGE_FORMATTER.format(minutes, "minute");
43 const answer = await $.ui.ask(`Another session edited ${path} ${age}. Edit it here too?`, {
44 header: "Ward",
45 options: WardAnswers,
46 });
47 if (answer === WardAnswer.Worktree)
48 return {
49 deny: `The person asked to move this work into a git worktree before editing ${path}: another session changed it ${age}. Enter a worktree, then carry on there.`,
50 };
51 else if (answer === WardAnswer.Cancel)
52 return { deny: `The person stopped this edit: another session changed ${path} ${age}.` };
53 else if (answer !== WardAnswer.Proceed) return { deny: `The person stopped this edit and said: ${answer}` };
54 }
55
56 const result = await proceed();
57 if (!result.deny && !result.isError) {
58 const records = getRecordsWithEdit(await readRecords($, recordsPath), path, {
59 editedAt: await $.clock.now(),
60 sessionId,
61 });
62 await $.fs.write(recordsPath, JSON.stringify(records));
63 }
64
65 return result;
66};
67
68// Any failure lets the edit through, the question dismissed or the record unreadable alike: a guard that blocked on
69// Its own failure would block every edit after it, and Cancel is the person's way to stop one. `next` is replay-safe
70// Here, so an edit the hook had already made is not made twice
71const letEditThrough = <E, R>(_$: EngineInterface, e: E, next: (e: E) => R): R => next(e);
72
73export const registerWard = (on: On): void => {
74 // eslint-disable-next-line no-restricted-syntax -- The engine's registration handler, run when the hook rejects, not a promise
75 on("tool.call", { tool: "Edit" }, ($, e, next) => ward($, e.file_path, () => next(e))).catch(letEditThrough);
76 // eslint-disable-next-line no-restricted-syntax -- The engine's registration handler, run when the hook rejects, not a promise
77 on("tool.call", { tool: "Write" }, ($, e, next) => ward($, e.file_path, () => next(e))).catch(letEditThrough);
78 // eslint-disable-next-line no-restricted-syntax -- The engine's registration handler, run when the hook rejects, not a promise
79 on("tool.call", { tool: "NotebookEdit" }, ($, e, next) => ward($, e.notebook_path, () => next(e))).catch(
80 letEditThrough,
81 );
82};
83src/services/constants.ts 46 lines1// The game's own interface gold, the accent wherever the persona publishes no character
2export const ACCENT_COLOR = "#d3bc8e";
3// The prompt cache's lifetime, which the engine chooses and does not report: an hour on a subscription
4export const CACHE_LIFETIME_MS: number = Temporal.Duration.from({ hours: 1 }).total("milliseconds");
5export const CACHE_LOW_MS: number = Temporal.Duration.from({ minutes: 10 }).total("milliseconds");
6export const CACHE_WARNING_MS: number = Temporal.Duration.from({ minutes: 5 }).total("milliseconds");
7export const CLOCK_TICK_MS: number = Temporal.Duration.from({ minutes: 1 }).total("milliseconds");
8export const USAGE_WARNING_PERCENTAGE = 80;
9export const USAGE_RESERVE_PERCENTAGE = 90;
10export const MAX_WAYPOINTS = 3;
11export const MAX_SHOWN_TASKS = 8;
12// Whole minutes as the band writes them, `12m`
13export const MINUTE_FORMATTER: Intl.NumberFormat = new Intl.NumberFormat("en", {
14 style: "unit",
15 unit: "minute",
16 unitDisplay: "narrow",
17});
18export const WARD_WINDOW_MS: number = Temporal.Duration.from({ minutes: 30 }).total("milliseconds");
19export const NO_WAYPOINTS_ANSWER = "NONE";
20// oxlint-disable-next-line typescript/no-inferrable-types -- `isolatedDeclarations` demands the annotation this template literal would otherwise infer
21export const WAYPOINTS_QUESTION: string = `Leave the conversation as it is and answer one side question. List the next steps worth taking from here, at most ${MAX_WAYPOINTS}, most useful first: each one short imperative line the person could send you as their next prompt, with no numbering, no markup and nothing else. When the work is finished or waits on the person, answer ${NO_WAYPOINTS_ANSWER}.`;
22export const WARM_QUESTION = "Answer with the one word OK.";
23export const HANDOFF_QUESTION =
24 "Write a handoff of this session for a fresh conversation that will carry on the work with no other context. Write it as the prompt that conversation starts with: the goal, what is done, what is next in order, the files, commands and decisions that matter with their reasons, and any open question. Be complete but compact, plain markdown, nothing before or after it.";
25export const VEIL_SECTION_ID = "genshin-mods-veil";
26export const VEIL_SYSTEM_SECTION =
27 "Recording mode is on: the screen is being recorded or shared. Never write an email address, a phone number, a money amount, an API key, a token or a password in a reply; write a placeholder such as [email], [phone], [amount] or [secret] instead, even when asked for the value. Tool calls still use the real values.";
28export const RESERVE_SECTION_ID = "genshin-mods-reserve";
29// The reserve's section ends on this instruction, the same for every window so only the window and its reset time vary
30export const RESERVE_INSTRUCTION =
31 "Until it resets, start no new agent or workflow that writes code or designs; have each running one commit what builds and end on a handoff spec; commit and push the work in hand; write every open item down with what it takes to resume it cold (its paths, what is done, the calls made, the next step); clean up idle servers, shells and monitors; keep only long-running compute going.";
32// The tools that look something up, the tools that change files, and the shells whose command decides which it is
33export const LOOKUP_TOOLS: readonly string[] = ["Glob", "Grep", "Read", "WebFetch"];
34export const RESET_TOOLS: readonly string[] = ["Agent", "Edit", "NotebookEdit", "Write"];
35export const SHELL_TOOLS: readonly string[] = ["Bash", "PowerShell"];
36// The `export` and `cd` a session chains ahead of its command, each stripped as one leading segment
37export const LEADING_SEGMENT_REGEX = /^\s*(?:export\s[^&;]*&&|cd\s[^&;]*&&|cd\s[^&;]*;)\s*/u;
38// The commands that only read, a whole first word each so `catalog` is not `cat`. No interpreter is one, since what
39// It runs may write as readily as read; PowerShell's cmdlets are matched in any case, as PowerShell matches them
40export const LOOKUP_COMMAND_REGEX =
41 /^\s*(?:cat|sed|grep|rg|find|ls|head|tail|awk|wc|get-content|get-childitem|select-string|git\s+(?:show|log|diff)|gh\s+run\s+view)(?:\s|$)/iu;
42// Every third lookup in a row, the chain a haiku agent should answer as one bounded question
43export const DELEGATION_NUDGE_EVERY = 3;
44export const DELEGATION_NUDGE =
45 "Third lookup in a row with no decision between them: by the llm-delegation skill's hop rule this chain goes to a haiku agent as one bounded question.";
46src/services/InitialState.ts 17 lines1import type { PluginState } from "claude-code";
2
3// Every state value's initial, read by each file's atoms: the engine traces a state reference only to an atom made in
4// The file that uses it, so each file makes its own and this is the one place their initials are written
5export const InitialState: PluginState["genshin-mods"] = {
6 commission: { goal: "", openedAt: 0, tasks: [] },
7 enabledMods: { commission: true, resin: true, veil: false, ward: true, waypoints: true },
8 isCommissionExpanded: false,
9 isHandingOff: false,
10 lastCacheRequestAt: 0,
11 lastPrompt: "",
12 lookupStreak: 0,
13 now: 0,
14 reserveWindow: { name: "", resetsAt: "" },
15 waypoints: [],
16};
17src/services/resin/getResinFigures.ts 45 lines1import type { SessionUsage } from "claude-code";
2
3import type { ResinFigure } from "../../models/ResinFigure";
4
5import { CACHE_LOW_MS, MINUTE_FORMATTER, USAGE_WARNING_PERCENTAGE } from "../constants";
6import { getCacheRemainingMs } from "./getCacheRemainingMs";
7
8const TOKEN_FORMATTER = new Intl.NumberFormat("en", { maximumFractionDigits: 1, notation: "compact" });
9const USD_CURRENCY_FORMATTER = new Intl.NumberFormat("en", { currency: "USD", style: "currency" });
10const RateLimitLabelMap: Record<string, string> = { five_hour: "5h", seven_day: "7d" };
11
12// The row's figures in the order they are read: the cache only once a request has started its clock, and each other
13// Figure only once the engine has a reading, since a zero it never measured would read as a fact
14export const getResinFigures = (
15 { context, cost, rateLimits }: Pick<SessionUsage, "context" | "cost" | "rateLimits">,
16 lastCacheRequestAt: number,
17 now: number,
18): ResinFigure[] => {
19 const figures: ResinFigure[] = [];
20 if (lastCacheRequestAt > 0) {
21 const remainingMs = getCacheRemainingMs(lastCacheRequestAt, now);
22 const remainingMinutes = Math.ceil(Temporal.Duration.from({ milliseconds: remainingMs }).total("minutes"));
23 const remaining = remainingMs > 0 ? MINUTE_FORMATTER.format(remainingMinutes) : "cold";
24 const resend = context.tokens === undefined ? "" : ` · re-sends ${TOKEN_FORMATTER.format(context.tokens)}`;
25 figures.push({ isWarning: remainingMs < CACHE_LOW_MS, label: "cache", text: `${remaining}${resend}` });
26 }
27
28 if (context.tokens !== undefined)
29 figures.push({
30 isWarning: (context.percent ?? 0) >= USAGE_WARNING_PERCENTAGE,
31 label: "context",
32 text: `${TOKEN_FORMATTER.format(context.tokens)}/${TOKEN_FORMATTER.format(context.window)}`,
33 });
34
35 for (const { kind, percentUsed } of rateLimits)
36 figures.push({
37 isWarning: percentUsed >= USAGE_WARNING_PERCENTAGE,
38 label: RateLimitLabelMap[kind] ?? kind,
39 text: `${Math.round(percentUsed)}%`,
40 });
41
42 if (cost) figures.push({ isWarning: false, label: "cost", text: USD_CURRENCY_FORMATTER.format(cost.usd) });
43 return figures;
44};
45src/services/band/getCommissionSummary.ts 22 lines1import type { Commission } from "../../../types";
2
3import { MINUTE_FORMATTER } from "../constants";
4
5const PERCENT_FORMATTER = new Intl.NumberFormat("en", { style: "percent" });
6
7// `goal · 3 of 4 · 75% · 12m`, the clock counting whole minutes from the commission's opening
8export const getCommissionSummary = ({ goal, openedAt, tasks }: Commission, now: number): string => {
9 const done = tasks.filter(({ status }) => status === "completed").length;
10 const elapsedMinutes = Math.floor(
11 Temporal.Duration.from({ milliseconds: Math.max(0, now - openedAt) }).total("minutes"),
12 );
13 return [
14 goal,
15 `${done} of ${tasks.length}`,
16 PERCENT_FORMATTER.format(done / tasks.length),
17 MINUTE_FORMATTER.format(elapsedMinutes),
18 ]
19 .filter(Boolean)
20 .join(" · ");
21};
22src/models/TaskCall.ts 15 lines1import type { CommissionTask } from "../../types";
2import type { TaskTool } from "./TaskTool";
3
4// One task tool call with what its result says, in the shapes the engine's own tools declare
5export type TaskCall =
6 | {
7 id: string;
8 // oxlint-disable-next-line literal-union/no-string-literal-union -- The engine's own TaskUpdate statuses
9 status?: "deleted" | CommissionTask["status"];
10 subject?: string;
11 tool: TaskTool.TaskUpdate;
12 }
13 | { id: string; subject: string; tool: TaskTool.TaskCreate }
14 | { todos: { content: string; status: CommissionTask["status"] }[]; tool: TaskTool.TodoWrite };
15