@litfamily/litclaude 1.0.12 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +117 -83
- package/README_ko-KR.md +88 -58
- package/bin/litclaude-ai.js +3 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +81 -2
- package/docs/privacy.md +6 -1
- package/package.json +1 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-hook.js +53 -2
- package/plugins/litclaude/bin/litclaude-hud.js +29 -1
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/lib/hud-accent-palette.mjs +33 -0
- package/plugins/litclaude/lib/jev-skill-hint.mjs +445 -0
package/docs/hooks.md
CHANGED
|
@@ -103,7 +103,7 @@ silent no-op for callers that still invoke it directly.
|
|
|
103
103
|
| Event | Runner | Purpose |
|
|
104
104
|
| --- | --- | --- |
|
|
105
105
|
| `SessionStart` | `plugins/litclaude/bin/litclaude-hook.js session-start` | Runs the bounded foreground automatic-update barrier on a fresh session, discovers repo-local rule files, names the newest valid `plans/<slug>.md`, and on `source: compact` spends one of two durable re-injection reservations at reduced caps. |
|
|
106
|
-
| `UserPromptSubmit` | `plugins/litclaude/bin/litclaude-hook.js user-prompt-submit` | Detects prompt routes and injects workflow context; independently delivers static rules not already recorded for this session. |
|
|
106
|
+
| `UserPromptSubmit` | `plugins/litclaude/bin/litclaude-hook.js user-prompt-submit` | Detects prompt routes and injects workflow context; independently delivers static rules not already recorded for this session; with `LITCLAUDE_JEV=1` and `TYPESAFE_API_KEY` set, adds at most one Jev skill hint line to an unrouted turn. |
|
|
107
107
|
| `PreToolUse` | `plugins/litclaude/bin/litclaude-hook.js pre-tool-use` | Enforces semantic action/root grants before Write, Edit, MultiEdit, NotebookEdit, Bash, Agent, and bounded read tools execute; denies supported reader-facing text writes with a humanizer block-tier finding. |
|
|
108
108
|
| `PostToolUse` | `plugins/litclaude/bin/litclaude-hook.js post-tool-use` | Names the post-edit checks the edit actually earned, injects any glob-scoped rule matching the edited paths, and checks successfully created DOCX/PPTX/PDF files for humanizer findings, including paths reported by Bash output. |
|
|
109
109
|
| `Stop` | `plugins/litclaude/bin/litclaude-hook.js stop` | Holds a lit-plan turn with no persisted plan and a lit workflow turn that edited interface files without a clean interface-probe run (each at most twice per turn), emits bounded start-work continuation on new progress, otherwise applies the opt-in litgoal autoloop gate. |
|
|
@@ -185,6 +185,83 @@ the host-owned SessionStart hook is the lifecycle exception to the TTY check.
|
|
|
185
185
|
The detached registry cache refresh described in the update-notifier section is
|
|
186
186
|
advisory and never performs an install.
|
|
187
187
|
|
|
188
|
+
### Jev skill hint (optional)
|
|
189
|
+
|
|
190
|
+
`plugins/litclaude/lib/jev-skill-hint.mjs` is off unless both `LITCLAUDE_JEV=1` and a
|
|
191
|
+
non-empty `TYPESAFE_API_KEY` are in the hook's environment. The key is read from the
|
|
192
|
+
environment only; the hook never reads a key file and never writes, prints, or traces the key.
|
|
193
|
+
|
|
194
|
+
A turn is eligible only when the deterministic router chose no route, the prompt is not a
|
|
195
|
+
slash command, a `!` shell line, or a host-expanded command, it names no skill (a
|
|
196
|
+
`litclaude:` name or a hyphenated or `lit`-prefixed catalog ID), and at least four
|
|
197
|
+
non-space characters remain after host notification blocks are removed. The exact bare
|
|
198
|
+
routes, the trusted start-work resume, and the diagnostic literal prompt are never eligible.
|
|
199
|
+
|
|
200
|
+
An eligible turn sends one `POST https://api.typesafe.ai/v1/systemone` with no retry and
|
|
201
|
+
`redirect: "error"`, so a redirect fails the request instead of being followed. The body holds
|
|
202
|
+
only `model`, `state`, and `questions`. `state` is built from the first 8,000 characters of the
|
|
203
|
+
prompt: the key's literal value becomes `[secret]`, home paths (`/Users/<name>`,
|
|
204
|
+
`/home/<name>`, `C:\Users\<name>`, with or without a trailing slash) become `~`, e-mail
|
|
205
|
+
addresses become `[email]`, and token shapes (`sk-`, `sk-ant-`, `ghp_`, `gho_`,
|
|
206
|
+
`github_pat_`, `npm_`, `apikey_`, `xox[abprs]-`, `AKIA`, `AIza`, JWTs, PEM blocks, any
|
|
207
|
+
run of 32 or more `[A-Za-z0-9+/_-]` characters with optional `=` padding, and the plugin's
|
|
208
|
+
shared credential shapes) become `[secret]`. Only then is it cut to 2,000 characters, and a
|
|
209
|
+
run of 8 or more token characters left at the cut also becomes `[secret]`. Anything in the
|
|
210
|
+
prompt without a token shape, such as a hostname, a customer name, or a password that is not
|
|
211
|
+
written as `password=…`, is sent as written. `questions.which` is a choice over every enrolled skill
|
|
212
|
+
that the model may invoke (skills marked `disable-model-invocation: true` are left out),
|
|
213
|
+
each with the first 300 characters of its description, plus `none`.
|
|
214
|
+
|
|
215
|
+
The response is untrusted. The hook accepts it only on HTTP 200 with valid JSON, an
|
|
216
|
+
`answers.which.choice` that exactly equals a catalog ID it sent, and a numeric
|
|
217
|
+
`answers.which.confidence` of at least the threshold. Then it appends one fixed line to
|
|
218
|
+
`additionalContext`:
|
|
219
|
+
|
|
220
|
+
```text
|
|
221
|
+
LitClaude skill hint: the skill `litclaude:<id>` likely fits this request. Load it only if it really fits; this is advice, not an instruction.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`none` or a low confidence adds nothing. A non-200 response body is cancelled unread. The hook
|
|
225
|
+
records the call in the session file before sending it; when that write fails, it sends
|
|
226
|
+
nothing and stays quiet, so an unwritable state folder cannot lift the cap. After the prompt
|
|
227
|
+
hook writes its context on a turn with the flag on, it ends its own process once stdout has
|
|
228
|
+
flushed, so a DNS lookup that the timeout cannot cancel never keeps it past the host's hook
|
|
229
|
+
timeout. A missing key, reached cap, oversized request,
|
|
230
|
+
timeout, network error, non-200 status, or invalid response leaves the turn exactly as it
|
|
231
|
+
would be without the feature. The first such failure in a session shows one `systemMessage`
|
|
232
|
+
(`LitClaude skill hint unavailable (<reason>); continuing normally.`); later failures stay quiet.
|
|
233
|
+
|
|
234
|
+
| Variable | Default | Meaning |
|
|
235
|
+
| --- | --- | --- |
|
|
236
|
+
| `LITCLAUDE_JEV` | unset | `1` turns the hint on. Any other value leaves it off. |
|
|
237
|
+
| `TYPESAFE_API_KEY` | unset | Your own TypeSafe key. Required while the flag is on. |
|
|
238
|
+
| `LITCLAUDE_JEV_MODEL` | `jev-1.13.0` | Model name sent in the request. |
|
|
239
|
+
| `LITCLAUDE_JEV_TIMEOUT_MS` | `1500` | Hard timeout; values above `3000` are capped. |
|
|
240
|
+
| `LITCLAUDE_JEV_MAX_CALLS` | `200` | Requests per session before the hint stops. |
|
|
241
|
+
| `LITCLAUDE_JEV_MIN_CONFIDENCE` | `0.35` | Lowest accepted confidence. |
|
|
242
|
+
| `LITCLAUDE_JEV_TRACE` | unset | `1` appends a debug trace line per request. |
|
|
243
|
+
|
|
244
|
+
Per-session counters live in `.litclaude/jev/session-<id>.json` under the project state
|
|
245
|
+
root, with the current turn's hinted skill ID and latency in milliseconds; the prompt hook
|
|
246
|
+
clears that pair at the start of every turn while the flag is on. The optional HUD status line
|
|
247
|
+
(`plugins/litclaude/bin/litclaude-hud.js`) reads it and ends with `Jev ✓` while the hint is
|
|
248
|
+
on, `Jev ✓ <skill> <latency>` on a hinted turn, or `Jev ⚠ key missing` when the flag is on
|
|
249
|
+
without a key; it shows nothing while the flag is off and never shows the key or its length.
|
|
250
|
+
While on, the HUD paints `Jev` as a rainbow whose starting hue follows the clock, so each refresh
|
|
251
|
+
shifts it slightly (truecolor, 256- and 16-colour tiers; plain under `NO_COLOR`, the HUD's
|
|
252
|
+
no-colour switch, or a light or unknown appearance). The session file also records that the
|
|
253
|
+
once-per-session `✦ Jev skill hint ON ✦` `systemMessage` was shown; the prompt hook writes that
|
|
254
|
+
mark before it emits the line on the first turn with the flag and key set, puts the line ahead
|
|
255
|
+
of any other visible message that turn, and shows no line when the mark cannot be written. The opt-in trace, `.litclaude/jev/trace.jsonl`, records the timestamp, the SHA-256 of the
|
|
256
|
+
redacted `state` (never of the raw prompt), chosen ID, confidence, latency, HTTP status, and fallback reason; it holds no prompt text,
|
|
257
|
+
key, or response body. Session and trace files are opened with `O_NOFOLLOW` (an `lstat` check
|
|
258
|
+
where the platform lacks it), so a symlink planted at either path is refused rather than written
|
|
259
|
+
through. Because `TYPESAFE_API_KEY` is exported in the shell that starts Claude Code, the
|
|
260
|
+
agent's own tools can read it too; use a key dedicated to this feature, with a low spend limit.
|
|
261
|
+
`litclaude doctor` prints `Jev skill hint: off`, `Jev skill hint: on`,
|
|
262
|
+
or `Jev skill hint: flag on but TYPESAFE_API_KEY missing`. The offline regression tests,
|
|
263
|
+
which replace `fetch` in the real hook process, live in `test/jev-skill-hint.test.mjs`.
|
|
264
|
+
|
|
188
265
|
## Bounded-authority start-work lifecycle
|
|
189
266
|
|
|
190
267
|
The hook and CLI share `plugins/litclaude/lib/start-work-lifecycle.mjs`.
|
|
@@ -464,7 +541,9 @@ host-dependent and need fallbacks.
|
|
|
464
541
|
|
|
465
542
|
Hooks parse JSON from stdin and return JSON to Claude Code. The hook does not
|
|
466
543
|
execute prompt text and does not echo prompt text into the returned context;
|
|
467
|
-
prompt text is not executed or echoed. The
|
|
544
|
+
prompt text is not executed or echoed. The optional Jev skill hint is the one network
|
|
545
|
+
call on this path: it is off by default, sends only redacted prompt text, and returns a fixed
|
|
546
|
+
sentence rather than any response text. The litwork detector returns constant
|
|
468
547
|
workflow guidance, so a prompt cannot become a shell command through the hook
|
|
469
548
|
response. Malformed hook input and malformed litgoal JSON should surface a controlled error instead of a misleading success response.
|
|
470
549
|
|
package/docs/privacy.md
CHANGED
|
@@ -13,6 +13,7 @@ transcript, model request, or diagnostic report contains no sensitive informatio
|
|
|
13
13
|
| Package installation | npm and its configured registry retrieve package metadata and package bytes; ordinary package-manager connection metadata is visible to those services. Global installation can run LitClaude's postinstall setup. | `LITCLAUDE_AUTO_INSTALL=0` or `LITCLAUDE_POSTINSTALL_SKIP=1` skips automatic plugin/HUD setup. It does not stop npm from fetching the package. |
|
|
14
14
|
| Update notice | Interactive `install`, `update`, and `doctor` can schedule an HTTPS GET to `registry.npmjs.org` for the package's latest-version metadata. The request identifies the package and sends an update-notifier user-agent; the request body does not contain project files or prompts. | Attempts are throttled using a 24-hour local cache. `NO_UPDATE_NOTIFIER` or `LITCLAUDE_NO_UPDATE_CHECK` disables the notice/check path, even when the variable is empty. CI, JSON, dry-run, and non-TTY management calls are gated. |
|
|
15
15
|
| Automatic update | An eligible cached newer version can trigger npm to fetch and run the installer, then doctor. This can occur on `SessionStart` or interactive management commands. | `LITCLAUDE_NO_AUTO_UPDATE` disables automatic installation but leaves update checks eligible. `LITCLAUDE_NO_UPDATE_CHECK` or `NO_UPDATE_NOTIFIER` disables both paths. SessionStart is a piped host hook, so it does not use the management TTY requirement. |
|
|
16
|
+
| Jev skill hint | Off by default. With `LITCLAUDE_JEV=1` and your own `TYPESAFE_API_KEY`, the `UserPromptSubmit` hook sends one HTTPS POST per eligible prompt to `api.typesafe.ai`. The body holds the prompt truncated to 2,000 characters with home paths, e-mail addresses, and token-shaped strings redacted, plus the list of LitClaude skill names and descriptions. Anything in the prompt without a token shape, such as a hostname, a customer name, or a password not written as `password=…`, is sent as written. No files, tool output, or history are sent. TypeSafe bills the request to your account (about $0.04 per million input tokens). | Unset `LITCLAUDE_JEV` to turn it off. Slash commands, routed prompts, and prompts that name a skill are never sent. Each request has a 1.5-second timeout, no retry, no redirect, and a per-session cap of 200. Because `TYPESAFE_API_KEY` is exported in the shell that starts Claude Code, the agent's own tools can read it; use a key dedicated to this feature, with a low spend limit. |
|
|
16
17
|
| Public source reader | `litclaude public-read` resolves DNS and sends HTTP(S) GET requests to the supplied public URL and validated redirects. Plain search text becomes a query to `duckduckgo.com/html/`. Requests use a LitClaude user-agent and Accept header. The destination sees the requested path and query, plus normal connection metadata. | The reader rejects credential-bearing URLs and private-network targets, revalidates redirects, and stops at detected authentication/paywall barriers. It does not use a browser login or cookie jar. Redacting a URL in returned evidence does not remove query values from the actual request. |
|
|
17
18
|
|
|
18
19
|
For a process that should avoid LitClaude background update traffic, set
|
|
@@ -41,7 +42,10 @@ the features used, it contains session/rule receipts, plan and goal state, work
|
|
|
41
42
|
ledgers, saved knowledge, and other feature-specific records. SessionStart
|
|
42
43
|
context-pressure detection can inspect a supplied transcript path up to 256 KiB
|
|
43
44
|
for a fixed set of pressure markers; it returns only a bounded advisory. No skill
|
|
44
|
-
review reads transcript excerpts or sends them to a model.
|
|
45
|
+
review reads transcript excerpts or sends them to a model. The optional Jev skill hint keeps
|
|
46
|
+
per-session request counts, the current turn's hinted skill ID and latency (shown by the
|
|
47
|
+
HUD), and whether the once-per-session "on" line was shown in `.litclaude/jev/` and, only with `LITCLAUDE_JEV_TRACE=1`, a trace
|
|
48
|
+
of hashes of the redacted prompt, timings, and status codes without prompt text. Existing
|
|
45
49
|
`pending-review.json` and `skill-loop-state.json` files are inert after the
|
|
46
50
|
automatic review feature's removal and may be deleted. No other state is affected.
|
|
47
51
|
|
|
@@ -76,6 +80,7 @@ for sensitive reports and [support guidance](../SUPPORT.md) for routine issues.
|
|
|
76
80
|
- [Public requests and redirects](../plugins/litclaude/lib/public-source-reader/routes.mjs),
|
|
77
81
|
[target validation](../plugins/litclaude/lib/public-source-reader/validator.mjs), and
|
|
78
82
|
[receipt redaction](../plugins/litclaude/lib/public-source-reader/receipts.mjs).
|
|
83
|
+
- [Jev skill hint request, redaction, and validation](../plugins/litclaude/lib/jev-skill-hint.mjs).
|
|
79
84
|
- [Project state-root resolution](../plugins/litclaude/lib/project-state-root.mjs),
|
|
80
85
|
[rules session receipts](../plugins/litclaude/lib/rules/session-state.mjs), and
|
|
81
86
|
[litgoal state](../plugins/litclaude/lib/litgoal/state.mjs).
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
|
4
4
|
import { dirname, extname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { standard, micro, colorMode, supportsBlocks } from "../lib/lit-mark.mjs";
|
|
7
|
-
import { NEON_END, NEON_START } from "../lib/hud-accent-palette.mjs";
|
|
7
|
+
import { NEON_END, NEON_START, rainbowText } from "../lib/hud-accent-palette.mjs";
|
|
8
8
|
import { ignitionMark, writeIgnitionState } from "../lib/hud-ignition.mjs";
|
|
9
9
|
import { renameAliases, renameNote } from "../lib/rename-aliases.mjs";
|
|
10
10
|
import { hasInterfaceModeWord, interfaceMode } from "../lib/interface-mode.mjs";
|
|
@@ -41,6 +41,8 @@ import { formatResolvedPlanNotice, resolveLatestDurablePlan } from "../lib/durab
|
|
|
41
41
|
import { clearLitPlanTurn, evaluateLitPlanStop, recordLitPlanTurn } from "../lib/lit-plan-persistence.mjs";
|
|
42
42
|
import { evaluateInterfaceProbeStop, observeInterfaceProbeTool, recordInterfaceProbeTurn } from "../lib/interface-probe-gate.mjs";
|
|
43
43
|
import { evaluateMotionStop, observeMotionTool, recordMotionTurn } from "../lib/motion-render-gate.mjs";
|
|
44
|
+
import { canonicalSkillIds } from "../lib/canonical-skill-catalog.mjs";
|
|
45
|
+
import { JEV_BANNER, JEV_FLAG, claimJevBanner, clearJevTurn, suggestJevSkill } from "../lib/jev-skill-hint.mjs";
|
|
44
46
|
|
|
45
47
|
const formatDurablePlanNotice = (cwd) => {
|
|
46
48
|
try {
|
|
@@ -124,7 +126,13 @@ const hookEventNames = {
|
|
|
124
126
|
"subagent-stop": "SubagentStop",
|
|
125
127
|
};
|
|
126
128
|
|
|
129
|
+
// Set at most once per session by the prompt hook; it leads that turn's visible message. The host
|
|
130
|
+
// prints its own label before the first line, so the one-line banner takes that line and any
|
|
131
|
+
// message that opens with a newline for its art starts right below it.
|
|
132
|
+
let jevBanner = null;
|
|
133
|
+
|
|
127
134
|
const writeContext = (additionalContext, systemMessage, shouldContinue = true) => {
|
|
135
|
+
if (jevBanner) systemMessage = systemMessage ? `${jevBanner}\n${systemMessage.replace(/^\n/u, "")}` : jevBanner;
|
|
128
136
|
const payload = {
|
|
129
137
|
continue: shouldContinue,
|
|
130
138
|
hookSpecificOutput: {
|
|
@@ -903,6 +911,31 @@ const findWorkflowTrigger = (prompt) => {
|
|
|
903
911
|
return undefined;
|
|
904
912
|
};
|
|
905
913
|
|
|
914
|
+
// A turn names a skill explicitly when it carries a qualified `litclaude:` name or a hyphenated
|
|
915
|
+
// or lit-prefixed catalog ID; plain words such as "refactor" or "rules" stay eligible.
|
|
916
|
+
const explicitSkillIds = canonicalSkillIds.filter((id) => id.includes("-") || id.startsWith("lit"));
|
|
917
|
+
const namesCatalogSkill = (raw) => raw.includes("litclaude:") || explicitSkillIds.some((id) => containsCompoundBoundedWord(raw, id));
|
|
918
|
+
|
|
919
|
+
// The optional Jev skill hint runs only on a turn the deterministic router left unrouted, and
|
|
920
|
+
// never on a slash command or a prompt that already names a skill. Any failure is quiet.
|
|
921
|
+
const jevTurnHint = async (prompt, input) => {
|
|
922
|
+
const quiet = { hint: null, note: null };
|
|
923
|
+
if (process.env[JEV_FLAG] !== "1" || isDiagnosticLiteralPrompt(prompt)) return quiet;
|
|
924
|
+
try {
|
|
925
|
+
const raw = rawTriggerText(prompt);
|
|
926
|
+
return await suggestJevSkill({
|
|
927
|
+
env: process.env,
|
|
928
|
+
prompt: withoutHostNotifications(prompt).trim(),
|
|
929
|
+
eligible: !containsSlashCommandMention(raw) && !namesCatalogSkill(raw),
|
|
930
|
+
sessionId: input.session_id,
|
|
931
|
+
stateRoot: resolveProjectStateRoot(typeof input.cwd === "string" ? input.cwd : process.cwd()),
|
|
932
|
+
pluginRoot,
|
|
933
|
+
});
|
|
934
|
+
} catch {
|
|
935
|
+
return quiet;
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
906
939
|
const isDiagnosticLiteralPrompt = (prompt) =>
|
|
907
940
|
/\bdiagnostic\b/iu.test(prompt)
|
|
908
941
|
&& /do not inspect or modify files/iu.test(prompt)
|
|
@@ -1258,6 +1291,14 @@ switch (eventName) {
|
|
|
1258
1291
|
try {
|
|
1259
1292
|
const cwd = typeof input.cwd === "string" ? input.cwd : process.cwd();
|
|
1260
1293
|
const stateRoot = resolveProjectStateRoot(cwd);
|
|
1294
|
+
// The HUD's Jev segment names a skill only for the turn that was hinted.
|
|
1295
|
+
if (process.env[JEV_FLAG] === "1") {
|
|
1296
|
+
clearJevTurn(stateRoot, input.session_id);
|
|
1297
|
+
if (claimJevBanner({ env: process.env, stateRoot, sessionId: input.session_id })) {
|
|
1298
|
+
const mode = colorMode({ isTTY: true });
|
|
1299
|
+
jevBanner = rainbowText(JEV_BANNER, { depth: mode === "none" ? "plain" : mode });
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1261
1302
|
if (trigger?.discipline === "lit-plan" && !trigger.safetyBlock) recordLitPlanTurn(stateRoot, { sessionId: input.session_id });
|
|
1262
1303
|
else clearLitPlanTurn(stateRoot);
|
|
1263
1304
|
recordInterfaceProbeTurn(stateRoot, { sessionId: input.session_id, discipline: trigger?.safetyBlock ? null : trigger?.discipline });
|
|
@@ -1307,7 +1348,17 @@ switch (eventName) {
|
|
|
1307
1348
|
["frontend-ui-ux", "visual-qa", "lit-diagram-drawer", "lit-pptx", "lit-docx", "lit-typographic-motion"].includes(trigger.discipline) ? 4096 : undefined,
|
|
1308
1349
|
), systemMessage);
|
|
1309
1350
|
} else {
|
|
1310
|
-
|
|
1351
|
+
const jev = await jevTurnHint(prompt, input);
|
|
1352
|
+
writeContext(
|
|
1353
|
+
withPromptRules(["LitClaude prompt hook checked: no workflow activation.", jev.hint].filter(Boolean).join("\n\n")),
|
|
1354
|
+
jev.note ?? undefined,
|
|
1355
|
+
);
|
|
1356
|
+
// A timed-out request can leave a DNS lookup that no signal cancels, which would keep this
|
|
1357
|
+
// process alive past the host's hook timeout. Exit once stdout has flushed the context.
|
|
1358
|
+
if (process.env[JEV_FLAG] === "1") {
|
|
1359
|
+
process.exitCode = 0;
|
|
1360
|
+
process.stdout.write("", () => setImmediate(() => process.exit()));
|
|
1361
|
+
}
|
|
1311
1362
|
}
|
|
1312
1363
|
break;
|
|
1313
1364
|
}
|
|
@@ -10,9 +10,12 @@ import {
|
|
|
10
10
|
hudColorDepth,
|
|
11
11
|
litBrandPrefix,
|
|
12
12
|
normalizeHudAppearance,
|
|
13
|
+
rainbowText,
|
|
13
14
|
} from "../lib/hud-accent-palette.mjs";
|
|
14
15
|
import { latestUsageTokens } from "../lib/cache-measurement.mjs";
|
|
15
16
|
import { readIgnitionState, renderIgnitionSegment } from "../lib/hud-ignition.mjs";
|
|
17
|
+
import { formatJevLatency, jevHudState } from "../lib/jev-skill-hint.mjs";
|
|
18
|
+
import { resolveProjectStateRoot } from "../lib/project-state-root.mjs";
|
|
16
19
|
|
|
17
20
|
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
21
|
const pluginManifestPath = join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
@@ -243,6 +246,30 @@ const formatUsageSegment = (label, value, reset) => {
|
|
|
243
246
|
return `${labelText} ${accent("[")}${makeBlockBar(pct, 2)}${accent("]")} ${pctText}${resetText ? ` ${text(resetText)}` : ""}`;
|
|
244
247
|
};
|
|
245
248
|
|
|
249
|
+
// Optional Jev skill hint: nothing while LITCLAUDE_JEV is off, and never the key or its length.
|
|
250
|
+
const jevSegment = (status) => {
|
|
251
|
+
let state;
|
|
252
|
+
try {
|
|
253
|
+
const cwd = typeof status.cwd === "string" && status.cwd ? status.cwd : null;
|
|
254
|
+
state = jevHudState({
|
|
255
|
+
env: process.env,
|
|
256
|
+
stateRoot: cwd ? resolveProjectStateRoot(cwd) : null,
|
|
257
|
+
sessionId: status.session_id,
|
|
258
|
+
});
|
|
259
|
+
} catch {
|
|
260
|
+
return "";
|
|
261
|
+
}
|
|
262
|
+
if (!state) return "";
|
|
263
|
+
if (state.keyMissing) return `${text("Jev")} ${colors.yellow}⚠${colors.reset} ${text("key missing")}`;
|
|
264
|
+
// While on, the word shimmers: each refresh starts the rainbow at a hue taken from the clock.
|
|
265
|
+
// Light and unknown appearances keep it on the default foreground like the rest of the text.
|
|
266
|
+
const now = Number(process.env.LITCLAUDE_HUD_TEST_NOW);
|
|
267
|
+
const phase = Math.floor((Number.isFinite(now) && now > 0 ? now : Date.now()) / 100) % 360;
|
|
268
|
+
const word = coloredText ? rainbowText("Jev", { depth: colorDepth, phase }) : text("Jev");
|
|
269
|
+
const mark = `${word} ${colors.green}✓${colors.reset}`;
|
|
270
|
+
return state.last ? `${mark} ${text(state.last.skill)} ${text(formatJevLatency(state.last.latency_ms))}` : mark;
|
|
271
|
+
};
|
|
272
|
+
|
|
246
273
|
const main = async () => {
|
|
247
274
|
const input = await readStdin();
|
|
248
275
|
const status = input.trim() ? JSON.parse(input) : {};
|
|
@@ -267,7 +294,8 @@ const main = async () => {
|
|
|
267
294
|
const ignition = renderIgnitionSegment(readIgnitionState(status.session_id), {
|
|
268
295
|
depth: plainOutput ? "plain" : colorDepth,
|
|
269
296
|
});
|
|
270
|
-
const
|
|
297
|
+
const jev = jevSegment(status);
|
|
298
|
+
const line = `${prefix}${ignition ? ` ${ignition}` : ""} ${accent("|")} ${text(model)} ${accent(sep)} ${contextText}${usageText}${git ? ` ${accent(sep)} ${text("git")} ${text(git)}` : ""}${jev ? ` ${accent(sep)} ${jev}` : ""}`;
|
|
271
299
|
process.stdout.write(`${line}\n`);
|
|
272
300
|
|
|
273
301
|
const lastMessage = latestUserMessage(status.transcript_path);
|
|
@@ -113,3 +113,36 @@ export const litBrandPrefix = (version, { noColor = false, truecolor = false, de
|
|
|
113
113
|
out += `\x1b[38;2;${NEON_END[0]};${NEON_END[1]};${NEON_END[2]}m v${version}]\x1b[0m`;
|
|
114
114
|
return out;
|
|
115
115
|
};
|
|
116
|
+
|
|
117
|
+
// --- Rainbow text (Jev skill hint) ------------------------------------------
|
|
118
|
+
// Per-character hue sweep starting at `phase` degrees: truecolor paints exact hues, 256-color
|
|
119
|
+
// snaps each hue to the 6x6x6 cube, 16-color picks the nearest bright hue, plain returns the
|
|
120
|
+
// text untouched. Spaces get no escape, and stripping the escapes always gives back `text`.
|
|
121
|
+
const RAINBOW_ANSI16 = Object.freeze([91, 93, 92, 96, 94, 95]);
|
|
122
|
+
|
|
123
|
+
const hueToRgb = (hue) => {
|
|
124
|
+
// HSV with a softened saturation so every hue stays legible on a dark terminal.
|
|
125
|
+
const h = ((hue % 360) + 360) % 360 / 60;
|
|
126
|
+
const s = 0.65;
|
|
127
|
+
const x = 1 - Math.abs((h % 2) - 1);
|
|
128
|
+
const [r, g, b] = [[1, x, 0], [x, 1, 0], [0, 1, x], [0, x, 1], [x, 0, 1], [1, 0, x]][Math.floor(h) % 6];
|
|
129
|
+
return [r, g, b].map((channel) => Math.round(255 * (1 - s + s * channel)));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const rainbowText = (text, { depth = "plain", phase = 0 } = {}) => {
|
|
133
|
+
if (!["truecolor", "256", "16"].includes(depth)) return text;
|
|
134
|
+
const chars = [...text];
|
|
135
|
+
const step = chars.length > 1 ? Math.min(60, 300 / (chars.length - 1)) : 0;
|
|
136
|
+
const painted = chars.map((char, index) => {
|
|
137
|
+
if (char === " ") return char;
|
|
138
|
+
const hue = (((phase + index * step) % 360) + 360) % 360;
|
|
139
|
+
if (depth === "16") return `\x1b[${RAINBOW_ANSI16[Math.floor(((hue + 30) % 360) / 60)]}m${char}`;
|
|
140
|
+
const [r, g, b] = hueToRgb(hue);
|
|
141
|
+
if (depth === "256") {
|
|
142
|
+
const cube = (value) => Math.round((value / 255) * 5);
|
|
143
|
+
return `\x1b[38;5;${16 + 36 * cube(r) + 6 * cube(g) + cube(b)}m${char}`;
|
|
144
|
+
}
|
|
145
|
+
return `\x1b[38;2;${r};${g};${b}m${char}`;
|
|
146
|
+
});
|
|
147
|
+
return `\x1b[1m${painted.join("")}\x1b[0m`;
|
|
148
|
+
};
|