@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/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 litwork detector returns constant
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. Existing
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/litclaude",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Claude Code-native workflow distribution.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "litclaude",
3
3
  "description": "Claude Code-native workflow plugin with a bounded-authority start-work lifecycle.",
4
- "version": "1.0.12",
4
+ "version": "1.0.13",
5
5
  "author": {
6
6
  "name": "LitClaude contributors"
7
7
  },
@@ -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
- writeContext(withPromptRules("LitClaude prompt hook checked: no workflow activation."));
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 line = `${prefix}${ignition ? ` ${ignition}` : ""} ${accent("|")} ${text(model)} ${accent(sep)} ${contextText}${usageText}${git ? ` ${accent(sep)} ${text("git")} ${text(git)}` : ""}`;
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);
@@ -9,7 +9,7 @@ import {
9
9
  } from "../lib/wikify-knowledge.mjs";
10
10
 
11
11
  const protocolVersion = "2024-11-05";
12
- const serverVersion = "1.0.12";
12
+ const serverVersion = "1.0.13";
13
13
 
14
14
  const publicSourceReadTool = {
15
15
  name: "public_source_read",
@@ -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
+ };