@litfamily/litclaude 1.0.21 → 1.0.23

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +34 -92
  3. package/README_ko-KR.md +42 -95
  4. package/bin/icon-font.mjs +264 -0
  5. package/bin/litclaude-ai.js +155 -18
  6. package/docs/assets/readme/badge-version.svg +1 -1
  7. package/docs/hooks.md +131 -5
  8. package/docs/privacy.md +2 -2
  9. package/package.json +13 -3
  10. package/plugins/litclaude/.claude-plugin/plugin.json +22 -2
  11. package/plugins/litclaude/bin/litclaude-hook.js +13 -7
  12. package/plugins/litclaude/bin/litclaude-hud.js +36 -22
  13. package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
  14. package/plugins/litclaude/hooks/hooks.json +1 -0
  15. package/plugins/litclaude/hooks/lit-band-client.mjs +44 -0
  16. package/plugins/litclaude/hooks/lit-mod.mjs +314 -0
  17. package/plugins/litclaude/lib/auto-handoff.mjs +2 -0
  18. package/plugins/litclaude/lib/automatic-update-refresh.mjs +4 -0
  19. package/plugins/litclaude/lib/automatic-update.mjs +148 -1
  20. package/plugins/litclaude/lib/cache-measurement.mjs +62 -1
  21. package/plugins/litclaude/lib/canonical-skill-resources.mjs +9 -9
  22. package/plugins/litclaude/lib/hud-accent-palette.mjs +2 -2
  23. package/plugins/litclaude/lib/hud-ignition.mjs +7 -37
  24. package/plugins/litclaude/lib/interface-probe-gate.mjs +2 -1
  25. package/plugins/litclaude/lib/jev-skill-hint.mjs +2 -0
  26. package/plugins/litclaude/lib/lit-plan-persistence.mjs +2 -1
  27. package/plugins/litclaude/lib/litgoal/autoloop.mjs +2 -0
  28. package/plugins/litclaude/lib/litgoal/cli.mjs +2 -0
  29. package/plugins/litclaude/lib/litgoal/ledger.mjs +3 -0
  30. package/plugins/litclaude/lib/litgoal/state.mjs +4 -0
  31. package/plugins/litclaude/lib/mod/frame.mjs +454 -0
  32. package/plugins/litclaude/lib/mod/layout.mjs +53 -0
  33. package/plugins/litclaude/lib/mod/motion.mjs +101 -0
  34. package/plugins/litclaude/lib/mod/render.mjs +20 -0
  35. package/plugins/litclaude/lib/mod/settings.mjs +105 -0
  36. package/plugins/litclaude/lib/mod/skills.mjs +81 -0
  37. package/plugins/litclaude/lib/mod/state.mjs +411 -0
  38. package/plugins/litclaude/lib/mod/timeline.mjs +152 -0
  39. package/plugins/litclaude/lib/mod/view.mjs +255 -0
  40. package/plugins/litclaude/lib/motion-render-gate.mjs +2 -1
  41. package/plugins/litclaude/lib/office-runtime.mjs +6 -0
  42. package/plugins/litclaude/lib/project-state-root.mjs +34 -2
  43. package/plugins/litclaude/lib/rules/engine.mjs +2 -1
  44. package/plugins/litclaude/lib/rules/format.mjs +24 -3
  45. package/plugins/litclaude/lib/rules/session-state.mjs +2 -1
  46. package/plugins/litclaude/lib/secure-path-read.mjs +46 -0
  47. package/plugins/litclaude/lib/start-work-lifecycle.mjs +4 -0
  48. package/plugins/litclaude/lib/wikify-knowledge.mjs +2 -1
  49. package/plugins/litclaude/scripts/scaffold-plan.mjs +2 -1
  50. package/plugins/litclaude/skills/lit-team/scripts/team.mjs +2 -1
  51. package/plugins/litclaude/skills/lit-typographic-motion/scripts/motion-runtime.mjs +7 -0
package/docs/hooks.md CHANGED
@@ -102,7 +102,7 @@ silent no-op for callers that still invoke it directly.
102
102
 
103
103
  | Event | Runner | Purpose |
104
104
  | --- | --- | --- |
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 and, with automatic handoff on, reloads the fresh handoff once. |
105
+ | `SessionStart` | `plugins/litclaude/bin/litclaude-hook.js session-start` | Runs the bounded foreground automatic-update barrier on a fresh session (or starts the detached version-cache refresh when the cache is stale), 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 and, with automatic handoff on, reloads the fresh handoff once. |
106
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; answers the exact prompts `lit-handoff auto on <percent>`, `lit-handoff auto off` and `lit-handoff auto status` by changing or reporting the automatic handoff setting. |
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. |
@@ -182,8 +182,27 @@ commands. `--no-auto-update`, `LITCLAUDE_NO_AUTO_UPDATE`,
182
182
  `NO_UPDATE_NOTIFIER`, and `LITCLAUDE_NO_UPDATE_CHECK` disable it. CI, JSON,
183
183
  dry-run, non-TTY management commands, import, and tool surfaces remain no-op;
184
184
  the host-owned SessionStart hook is the lifecycle exception to the TTY check.
185
- The detached registry cache refresh described in the update-notifier section is
186
- advisory and never performs an install.
185
+
186
+ SessionStart also keeps that cache fresh. When the cache is missing or older than
187
+ 24 hours, and no attempt was recorded in the last 24 hours, the hook records the
188
+ attempt and starts a detached child
189
+ (`plugins/litclaude/lib/automatic-update-refresh.mjs`) that makes one GET for the
190
+ package's latest version, with a 3-second limit, no redirect and a 64 KiB cap. The
191
+ hook does not wait for it. The child writes `latest.json` only if its attempt still
192
+ owns the cache, and never installs anything. A cache file that is not LitClaude's own record, such as another package's, is left untouched; the next fresh session applies the
193
+ version through the transaction above. The request goes to the registry in
194
+ `npm_config_registry` when that is a plain origin, the same registry the install
195
+ uses, and to `registry.npmjs.org` otherwise. Every opt-out above stops the request
196
+ too. Offline, the attempt fails quietly and the next try waits a day.
197
+
198
+ Before installing, the hook reads `installed_plugins.json` under the Claude home. If
199
+ the registered LitClaude version is already the cached version or newer, it skips the
200
+ install. A long-lived Claude Code process keeps running the hook from the version
201
+ folder it started with, so without this check every new session in that process
202
+ would install the same version again. The hook also skips a version whose latest receipt
203
+ under `update-notifier/automatic/` is `rolled-back` or `failed` and less than 24 hours old,
204
+ so a version that just failed is retried once a day, not on every session. A newer version
205
+ is tried at once.
187
206
 
188
207
  ### Jev skill hint (optional)
189
208
 
@@ -262,9 +281,9 @@ through. The `.litclaude` and `.litclaude/jev` folders must be real folders too:
262
281
  is a symlink, the hint reads and writes nothing there, sends no request, and shows no hint or note. Because `TYPESAFE_API_KEY` is exported in the shell that starts Claude Code, the
263
282
  agent's own tools can read it too; use a key dedicated to this feature, with a low spend limit.
264
283
  `litclaude doctor` prints `Jev skill hint: off`, `Jev skill hint: on`,
265
- or `Jev skill hint: flag on but TYPESAFE_API_KEY missing`. The GitHub README shows each
284
+ or `Jev skill hint: flag on but TYPESAFE_API_KEY missing`. The Jev page shows each
266
285
  visible state in sample output under
267
- [What you will see](https://github.com/wjgoarxiv/litclaude#what-you-will-see). The offline regression tests,
286
+ [What you will see](https://github.com/wjgoarxiv/litclaude/blob/main/docs/jev.md#what-you-will-see). The offline regression tests,
268
287
  which replace `fetch` in the real hook process, live in `test/jev-skill-hint.test.mjs`.
269
288
 
270
289
  ## Automatic handoff
@@ -320,6 +339,113 @@ The pieces, in the order they run:
320
339
  project it runs in. The model still has to follow the block reason to write the file; the hook
321
340
  can require nothing more than that.
322
341
 
342
+ ## The LIT band (Claude Code mod)
343
+
344
+ From Claude Code 2.1.287 a plugin can ship a mod: a hooks module that draws inside Claude Code
345
+ itself. LitClaude's is `plugins/litclaude/hooks/lit-mod.mjs`, named under `modules` in
346
+ `hooks/hooks.json` next to the settings hooks above. It installs with the plugin and needs no
347
+ setup. The status line stays as it is; the band adds a live layer above the prompt.
348
+
349
+ From the first prompt to the end of the session a LIT card sits above the prompt. It keeps the
350
+ same four rows whether LitClaude is working or idle, and takes the status line's accent
351
+ (`LITCLAUDE_HUD_ACCENT`, read from the installed status line command), so the card and the
352
+ status line read as one family:
353
+
354
+ ```
355
+ ╭─ [🔥 LIT] · lit-loop ─────────────────────────── WORK ● ─╮
356
+ │ ● PLAN ━━ [WORK] ━━ ○ REVIEW goal ██░░░ 2/5 │
357
+ │ skills [⚡ lit-plan] [⚡ litresearch] [↯ lit-docx] │
358
+ ╰─ ctx 42% ▰▰▱▱▱ · 5h 31% · git main ──────────────────────╯
359
+ ```
360
+
361
+ The brackets stand for the ends of filled pills: rounded caps from the icon font with `nerd`
362
+ glyphs, one cell of the pill's colour with `basic`. The brand is a pink pill, the active phase
363
+ and the skills of this request are pills on the accent with dark text, and skills from earlier
364
+ requests are muted grey pills.
365
+
366
+ - The top edge names the discipline the prompt hook ignited (the last one, dimmed, while the
367
+ session is idle; `ready` before any) and, on the right, the current phase: `WORK ●`, `idle`,
368
+ or `DONE ✓` during the finish.
369
+ - The phase row lights `PLAN ━━ WORK ━━ REVIEW` from the start-work plan, the litgoal goal and
370
+ the discipline (the active phase as a pill, done phases `●`, the rest `○`), and on its right shows a pink-to-cyan progress bar with the checked tasks or
371
+ passed criteria (`goal ✓ 5/5` once a goal is met), the number of live subagents, the autoloop
372
+ count with the time left in its 30-minute budget, and `ctx 76% → 80%` when the context is
373
+ within five points of the auto-handoff percent.
374
+ - The chips row shows each LitFamily skill that fired as a pill: a Skill tool call naming a LitClaude
375
+ skill, a typed `/litclaude:<name>`, or bare `lit` routing seen through the ignition record.
376
+ A skill fired in the current request is bright, with a short light passing over its name as
377
+ it arrives; when the turn completes it dims into the `recent` list, and a new firing brightens
378
+ it again. Chips follow a dim `skills` label; those that do not fit collapse into a muted `+N`. Skills of other plugins never get a chip.
379
+ With no chip yet, the row shows the next task while work runs, or how to start (`lit <task>`)
380
+ while idle.
381
+ - The bottom edge carries the live figures from the status line: context fill, the five-hour
382
+ rate window and the Git branch (read from `HEAD`, across a linked worktree).
383
+ - A notice takes the chips row for a few seconds, with a toast, when a gate fires: lit-plan not
384
+ saved, interface probe pending or failed, motion gate, context near the auto-handoff percent,
385
+ auto-handoff fired. The completion moment takes it when the last task is checked, the work
386
+ completes or the goal is met: a light passes over the brand, embers scatter, and `COMPLETE`
387
+ shows for about a second.
388
+ - Amber or red when something needs attention: a lane quiet for more than 10 minutes turns the
389
+ border amber and puts `⚠ <lane> quiet for 12m` on the bottom edge; a failed interface probe
390
+ turns it red.
391
+
392
+ The card is the terminal's width up to 80 columns and every row is fitted to it, with Hangul and
393
+ emoji counted as two cells. Nothing wraps: on a narrow terminal the Git branch goes first, then
394
+ the rate window, the lanes, the autoloop count and the long phase labels (`P ━ W ━ R`).
395
+
396
+ The spinner gains ` · <discipline> · n/m` after its ellipsis. `/lit-evidence` opens a pane with
397
+ three tabs (keys `1`, `2`, `3`): the goal's criteria with their last evidence, blockers and the
398
+ newest litgoal ledger entries; the lanes; and the gates.
399
+
400
+ Motion: entrances pop in 60 ms apart with a slight overshoot (160 ms each), fills and colour
401
+ changes ease in and out (300 ms and 140 ms), notices slide in over 220 ms and leave over 150 ms,
402
+ a chip's arrival light runs 400 ms (a plain fade under `subtle`, nothing under `off`), and the
403
+ finish lasts at most 1.2 s. The band's `Client` element animates on Claude Code's frame
404
+ clock only while something moves and stops its timer as soon as everything settles. In Claude
405
+ Code 2.1.292 a terminal row whose only change is a non-ASCII glyph is not repainted, so every
406
+ moving part changes ASCII text or colour: bar cells are coloured spaces, embers are ASCII sparks,
407
+ a chip's light moves over its ASCII name, and a glyph swaps only together with its words.
408
+
409
+ Options, in `/config` under LitClaude (stored in `pluginConfigs`):
410
+
411
+ | Option | Values | Default |
412
+ | --- | --- | --- |
413
+ | `motion` | `full`, `subtle` (no overshoot, shimmer or embers, shorter), `off` (final frames only) | `full` |
414
+ | `glyphs` | `auto` (Nerd glyphs when the icon font is installed), `nerd`, or `basic` | `auto` |
415
+ | `band` | `always` (the whole session, idle included), `auto` (only while work is active or a skill of this request is lit), `off` | `always` |
416
+
417
+ Unknown values fall back to the default. The fields are plain strings with no `options` list on
418
+ purpose: Claude Code 2.1.270 and older refuse to load a plugin whose `userConfig` has one, and
419
+ that would take every LitClaude hook down with it. Nerd glyphs reach only the terminal; the
420
+ desktop app and the other surfaces always get the basic set.
421
+
422
+ The icons on `nerd` (the rounded pill caps U+E0B6 and U+E0B4, the chip bolt and the branch mark)
423
+ come from a Nerd Font. `litclaude-ai install
424
+ --icon-font`, or a Yes to the install question (default No), adds the symbols-only Nerd Font,
425
+ Symbols Nerd Font Mono (MIT), to the per-user font folder (`~/Library/Fonts` on macOS,
426
+ `~/.local/share/fonts` on Linux, then `fc-cache -f` if present; on Windows it prints how to
427
+ install it by hand). Your terminal font stays as it is: most macOS terminals take the missing
428
+ icons from the installed font through system font fallback, and the installer prints one line for
429
+ your terminal (from `TERM_PROGRAM`) in case it does not. The archive is the pinned Nerd Fonts
430
+ release asset, refused unless its SHA-256 matches; `LITCLAUDE_ICON_FONT_SOURCE` names an offline
431
+ copy of the same archive. The licence goes next to the font, a manifest under `~/.litclaude`
432
+ records the two files, `uninstall` removes only those (and only while unchanged), and `doctor`
433
+ prints `ICON_FONT:`. With `glyphs` on `auto`, the band checks those font folders by stat at session
434
+ start and says what it decided on the `/lit-evidence` gates tab.
435
+
436
+ The mod is read-only. It reads LitClaude's own state, the Git `HEAD` of the working directory and
437
+ the merged settings (for the status line's accent) and nothing else: the ignition record in the
438
+ per-user temp folder, `.litclaude/boulder.json` (found the way the hooks find it, across a
439
+ linked worktree), and under the outermost Git root's `.litclaude/` the litgoal goal, autoloop
440
+ counter and ledger, the lit-plan, interface-probe and motion turn records, and the auto-handoff
441
+ settings and session record. Each file must be a regular file under its size cap that resolves
442
+ exactly where its path says, so a symlinked file or state folder is skipped. It writes nothing,
443
+ starts no process, uses no network and calls no model. Its pure parts live in
444
+ `plugins/litclaude/lib/mod/` and are tested under Node.
445
+
446
+ On Claude Code releases before 2.1.287 the `modules` key is ignored and every settings hook runs
447
+ as before; there is no band.
448
+
323
449
  ## Bounded-authority start-work lifecycle
324
450
 
325
451
  The hook and CLI share `plugins/litclaude/lib/start-work-lifecycle.mjs`.
package/docs/privacy.md CHANGED
@@ -11,8 +11,8 @@ transcript, model request, or diagnostic report contains no sensitive informatio
11
11
  | Surface | Destination and data | Controls and limits |
12
12
  | --- | --- | --- |
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
- | 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
- | 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. |
14
+ | Update notice | Interactive `install`, `update`, and `doctor` can schedule an HTTPS GET to `registry.npmjs.org` for the package's latest-version metadata. A fresh Claude Code session (`SessionStart`) does the same in a detached background process when the cached result is missing or older than 24 hours; that request goes to `npm_config_registry` when it is set to a plain origin. 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
+ | 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 and the SessionStart check that feeds it, but leaves the interactive update notice eligible. An install is skipped when Claude Code already lists that version or a newer one. `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
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. |
17
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. |
18
18
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@litfamily/litclaude",
3
- "version": "1.0.21",
4
- "description": "Claude Code-native workflow distribution.",
3
+ "version": "1.0.23",
4
+ "description": "Claude Code plugin: add lit to a prompt and Claude plans, tests, checks and reviews the work, then leaves a handoff for the next session.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "litclaude-ai": "bin/litclaude-ai.js",
@@ -82,9 +82,19 @@
82
82
  },
83
83
  "keywords": [
84
84
  "claude-code",
85
+ "claude-code-plugin",
86
+ "claude",
85
87
  "plugin",
86
88
  "ai-agents",
87
- "orchestration"
89
+ "agent-workflow",
90
+ "orchestration",
91
+ "planning",
92
+ "code-review",
93
+ "research",
94
+ "tdd",
95
+ "context-handoff",
96
+ "korean",
97
+ "office-documents"
88
98
  ],
89
99
  "author": "LitClaude contributors",
90
100
  "license": "MIT",
@@ -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.21",
4
+ "version": "1.0.23",
5
5
  "author": {
6
6
  "name": "LitClaude contributors"
7
7
  },
@@ -13,5 +13,25 @@
13
13
  "mcpServers": "./.mcp.json",
14
14
  "lspServers": "./.lsp.json",
15
15
  "displayName": "LitClaude",
16
- "defaultEnabled": true
16
+ "defaultEnabled": true,
17
+ "userConfig": {
18
+ "motion": {
19
+ "type": "string",
20
+ "title": "LIT band motion",
21
+ "description": "How the band above the prompt moves: full, subtle (no overshoot, shimmer or embers) or off.",
22
+ "default": "full"
23
+ },
24
+ "glyphs": {
25
+ "type": "string",
26
+ "title": "LIT band glyphs",
27
+ "description": "auto uses the icon font when it is installed (litclaude-ai install --icon-font); nerd always; basic works with any font.",
28
+ "default": "auto"
29
+ },
30
+ "band": {
31
+ "type": "string",
32
+ "title": "LIT band",
33
+ "description": "When the LIT card shows above the prompt: always (the whole session, idle included), auto (only while LitClaude work is active or a skill of this request is lit) or off.",
34
+ "default": "always"
35
+ }
36
+ }
17
37
  }
@@ -744,11 +744,13 @@ const hasUiModeIntent = (normalized) => hasInterfaceModeWord(normalized) && uiSu
744
744
  // ("the report says", "deck of cards", "문서 참고"). Hangul terms are bare substrings for the
745
745
  // reason given above, except 덱, which would match inside 인덱스 and so needs a non-Hangul
746
746
  // neighbour. Code documentation and tool reports are excluded by name: a README, API docs,
747
- // a docstring, a bug, test, coverage or error report are not Word files.
747
+ // a docstring, a bug, test, coverage or error report are not Word files. A Markdown file is
748
+ // excluded only when it is a named project document or sits under docs/; any other `.md` is
749
+ // usually the source of the deck or report ("turn notes.md into slides").
748
750
  const officeVerb = /\b(?:make|making|made|create|creating|write|writing|draft|drafting|prepare|preparing|build|building|generate|generating|produce|producing|put\s+together|turn\b.*\binto|compile)\b|만들|만드|작성|써\s?줘|써\s?주|써서|써봐|쓰고|정리해|정리하|준비|초안|뽑아|짜\s?줘|구성해/u;
749
751
  const slideNoun = /\b(?:slides?|slide\s?deck|deck|presentation|pptx?|powerpoint|keynote)\b|발표|슬라이드|피피티|파워포인트|(?<![가-힣])덱(?![가-힣])|(?<![가-힣])덱을|(?<![가-힣])덱으로/u;
750
752
  const documentNoun = /\b(?:reports?|docx|doc|documents?|word\s+(?:doc(?:ument)?|file)|ms\s?word|white\s?papers?|proposals?|memos?)\b|보고서|리포트|기획서|제안서|계획서|문서|워드|백서/u;
751
- const codeDocumentation = /readme|changelog|contributing|api\s?(?:문서|docs?\b|reference)|docstrings?|jsdoc|javadoc|주석|\.md\b|\bdocs\/|\b(?:bug|test|coverage|error|crash|lint|issue|incident)\s+reports?\b|\breport\s+(?:a|the|this|that)\s+(?:bug|issue|error|problem)/u;
753
+ const codeDocumentation = /readme|changelog|contributing|api\s?(?:문서|docs?\b|reference)|docstrings?|jsdoc|javadoc|주석|\b(?:agents|claude|gemini|skill|security|code_of_conduct|license|authors|architecture|support|governance)\.md\b|\bdocs\/|\b(?:bug|test|coverage|error|crash|lint|issue|incident)\s+reports?\b|\breport\s+(?:a|the|this|that)\s+(?:bug|issue|error|problem)/u;
752
754
 
753
755
  // Returns the office skills a bare-lit prompt asks for, slides first when both are named.
754
756
  const officeIntent = (normalized) => {
@@ -1140,7 +1142,7 @@ const rulesBlockOrEmpty = (build) => {
1140
1142
  }
1141
1143
  };
1142
1144
 
1143
- const staticRulesContext = ({ cwd, sessionId, maxRuleChars, maxResultChars }) =>
1145
+ const staticRulesContext = ({ cwd, sessionId, maxRuleChars, maxResultChars, maxResultBytes }) =>
1144
1146
  rulesBlockOrEmpty(() =>
1145
1147
  staticRulesBlock({
1146
1148
  cwd,
@@ -1148,6 +1150,7 @@ const staticRulesContext = ({ cwd, sessionId, maxRuleChars, maxResultChars }) =>
1148
1150
  sessionId,
1149
1151
  ...(maxRuleChars === undefined ? {} : { maxRuleChars }),
1150
1152
  ...(maxResultChars === undefined ? {} : { maxResultChars }),
1153
+ ...(maxResultBytes === undefined ? {} : { maxResultBytes }),
1151
1154
  }),
1152
1155
  );
1153
1156
 
@@ -1254,11 +1257,12 @@ switch (eventName) {
1254
1257
  // Prompt-time static injection is the straggler lane: SessionStart already recorded
1255
1258
  // what it injected, so this is normally empty and only speaks when a rule file
1256
1259
  // appeared mid-session. Reduced budget, because it can fire on every turn.
1257
- const promptRulesFor = (maxResultChars = DEFAULT_PROMPT_MAX_RESULT_CHARS) => staticRulesContext({
1260
+ const promptRulesFor = (maxResultChars = DEFAULT_PROMPT_MAX_RESULT_CHARS, maxResultBytes) => staticRulesContext({
1258
1261
  cwd: typeof input.cwd === "string" ? input.cwd : process.cwd(),
1259
1262
  sessionId: input.session_id,
1260
1263
  maxRuleChars: Math.min(DEFAULT_PROMPT_MAX_RULE_CHARS, maxResultChars),
1261
1264
  maxResultChars,
1265
+ maxResultBytes,
1262
1266
  });
1263
1267
  // The exact-bare canonical routes and the trusted-resume authority envelope are
1264
1268
  // deliberately excluded: both are fixed payloads where an appended block would
@@ -1269,9 +1273,11 @@ switch (eventName) {
1269
1273
  ? undefined
1270
1274
  : maxBytes - Buffer.byteLength(context, "utf8") - separatorBytes;
1271
1275
  if (remainingBytes !== undefined && remainingBytes <= 0) return context;
1272
- const promptRules = promptRulesFor(remainingBytes === undefined
1273
- ? DEFAULT_PROMPT_MAX_RESULT_CHARS
1274
- : Math.floor(remainingBytes / 4));
1276
+ // Count the rules in real bytes: a worst-case bytes-per-char guess left a long install
1277
+ // path in the context with too little room for even a rule's first line.
1278
+ const promptRules = remainingBytes === undefined
1279
+ ? promptRulesFor()
1280
+ : promptRulesFor(Math.min(DEFAULT_PROMPT_MAX_RESULT_CHARS, remainingBytes), remainingBytes);
1275
1281
  if (!promptRules) return context;
1276
1282
  const combined = `${context}\n\n${promptRules}`;
1277
1283
  return maxBytes === undefined || Buffer.byteLength(combined, "utf8") <= maxBytes
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
- import { dirname, join } from "node:path";
5
+ import { basename, dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import {
8
8
  accentCodeForName,
@@ -13,8 +13,8 @@ import {
13
13
  rainbowText,
14
14
  toneText,
15
15
  } from "../lib/hud-accent-palette.mjs";
16
- import { latestUsageTokens } from "../lib/cache-measurement.mjs";
17
- import { readIgnitionState, renderIgnitionSegment } from "../lib/hud-ignition.mjs";
16
+ import { latestUsageTokensCached } from "../lib/cache-measurement.mjs";
17
+ import { ignitionStatePath } from "../lib/hud-ignition.mjs";
18
18
  import { jevHudState } from "../lib/jev-skill-hint.mjs";
19
19
  import { resolveProjectStateRoot } from "../lib/project-state-root.mjs";
20
20
  import { recordContextUsage, resolveAutoHandoff } from "../lib/auto-handoff.mjs";
@@ -77,6 +77,19 @@ const shortModel = (raw) => {
77
77
  return compact.slice(0, 10) || "?";
78
78
  };
79
79
 
80
+ // A million-token window reads as 1m, and smaller ones keep their k count.
81
+ const formatWindow = (size) => {
82
+ if (size >= 1000000 && size % 100000 === 0) return `${size / 1000000}m`;
83
+ return `${Math.max(1, Math.round(size / 1000))}k`;
84
+ };
85
+
86
+ // Claude Code sends effort.level only for models that take the effort parameter.
87
+ const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
88
+ const effortLabel = (status) => {
89
+ const level = status.effort?.level;
90
+ return EFFORT_LEVELS.has(level) ? `[${level}]` : "";
91
+ };
92
+
80
93
  const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, Number.isFinite(value) ? value : min));
81
94
 
82
95
  const displayPercent = (value) => Math.round(clamp(value));
@@ -111,7 +124,8 @@ const latestUserMessage = (transcriptPath) => {
111
124
  for (let index = lines.length - 1; index >= 0; index -= 1) {
112
125
  try {
113
126
  const event = JSON.parse(lines[index]);
114
- if (event.type !== "user") continue;
127
+ // Loaded skill bodies and compaction summaries are host-written user entries, not prompts.
128
+ if (event.type !== "user" || event.isMeta === true || event.isCompactSummary === true) continue;
115
129
  const content = event?.message?.content;
116
130
  const text = Array.isArray(content)
117
131
  ? content.filter((part) => part?.type === "text").map((part) => part.text).join(" ")
@@ -227,9 +241,9 @@ const formatReset = (value) => {
227
241
  const minutes = Math.max(0, Math.floor((target - Date.now()) / 60000));
228
242
  const hours = Math.floor(minutes / 60);
229
243
  const days = Math.floor(hours / 24);
230
- if (days > 0) return `↻${days}d${hours % 24}h`;
231
- if (hours > 0) return `↻${hours}h${minutes % 60}m`;
232
- return `↻${minutes}m`;
244
+ if (days > 0) return `↻ ${days}d${hours % 24}h`;
245
+ if (hours > 0) return `↻ ${hours}h${minutes % 60}m`;
246
+ return `↻ ${minutes}m`;
233
247
  };
234
248
 
235
249
  const coloredText = appearance === "dark";
@@ -239,13 +253,13 @@ const formatUsageSegment = (label, value, reset) => {
239
253
  const labelText = text(label);
240
254
  if (value === null) {
241
255
  return coloredText
242
- ? `${labelText} ${accent("[")}${colors.accent}░░${colors.reset}${accent("]")} ${accent("--%")}`
243
- : `${labelText} [${makeBlockBar(0, 2)}] --%`;
256
+ ? `${labelText} ${accent("[")}${colors.accent}░░${colors.reset}${accent("]")}${accent("--%")}`
257
+ : `${labelText} [${makeBlockBar(0, 2)}]--%`;
244
258
  }
245
259
  const pct = displayPercent(value);
246
260
  const resetText = formatReset(reset);
247
261
  const pctText = coloredText ? `${barColor(pct)}${pct}%${colors.reset}` : `${pct}%`;
248
- return `${labelText} ${accent("[")}${makeBlockBar(pct, 2)}${accent("]")} ${pctText}${resetText ? ` ${text(resetText)}` : ""}`;
262
+ return `${labelText} ${accent("[")}${makeBlockBar(pct, 2)}${accent("]")}${pctText}${resetText ? ` ${text(resetText)}` : ""}`;
249
263
  };
250
264
 
251
265
  // Optional Jev skill hint badge, attached to the model label: nothing while LITCLAUDE_JEV is
@@ -312,13 +326,12 @@ const main = async () => {
312
326
  const version = readVersion();
313
327
  const model = shortModel(status.model?.display_name ?? status.model?.id);
314
328
  const maxContext = Number(status.context_window?.context_window_size ?? 200000);
315
- const maxK = Math.max(1, Math.round(maxContext / 1000));
316
- const tokens = latestUsageTokens(status.transcript_path);
317
- const estimatedTokens = tokens > 0 ? tokens : 20000;
329
+ const windowText = formatWindow(maxContext);
318
330
  const statusContextPct = firstNumber(status.context_window?.used_percentage, status.context_window?.usage_percentage);
319
- const rawContextPct = statusContextPct === null ? Math.floor((estimatedTokens * 100) / maxContext) : statusContextPct;
320
- const contextPct = displayPercent(rawContextPct);
321
- const knownContextPct = statusContextPct !== null ? statusContextPct : tokens > 0 ? rawContextPct : null;
331
+ const usageCachePath = join(dirname(ignitionStatePath(status.session_id)), `usage-${basename(ignitionStatePath(status.session_id))}`);
332
+ const tokens = statusContextPct === null ? latestUsageTokensCached(status.transcript_path, usageCachePath) : 0;
333
+ // Without a host percent or a transcript usage record the size is unknown, and the line says so.
334
+ const knownContextPct = statusContextPct !== null ? statusContextPct : tokens > 0 ? Math.floor((tokens * 100) / maxContext) : null;
322
335
  const usage = parseUsage(status);
323
336
  const usageText = ` ${accent(sep)} ${formatUsageSegment("5h", usage.fiveHour, usage.fiveHourReset)} ${accent(sep)} ${formatUsageSegment("1w", usage.weekly, usage.weeklyReset)}`;
324
337
  const git = gitStatus(status.cwd);
@@ -327,13 +340,14 @@ const main = async () => {
327
340
  depth: colorDepth,
328
341
  appearance,
329
342
  });
330
- const contextText = `${text("ctx")} ${accent("[")}${makeContextBar(contextPct)}${accent("]")} ${text(`${contextPct}%/${maxK}k`)}`;
331
- const ignition = renderIgnitionSegment(readIgnitionState(status.session_id), {
332
- depth: plainOutput ? "plain" : colorDepth,
333
- });
343
+ const contextPct = knownContextPct === null ? null : displayPercent(knownContextPct);
344
+ const contextText = contextPct === null
345
+ ? `${text("ctx")} ${text(`?/${windowText}`)}`
346
+ : `${text("ctx")} ${accent("[")}${makeContextBar(contextPct)}${accent("]")} ${text(`${contextPct}%/${windowText}`)}`;
347
+ const effort = effortLabel(status);
334
348
  const jev = jevBadge(status);
335
349
  const autoHandoff = autoHandoffSegment(status, knownContextPct);
336
- const line = `${prefix}${ignition ? ` ${ignition}` : ""} ${accent("|")} ${text(model)}${jev ? ` ${jev}` : ""} ${accent(sep)} ${contextText}${autoHandoff ? ` ${accent(sep)} ${autoHandoff}` : ""}${usageText}${git ? ` ${accent(sep)} ${text("git")} ${text(git)}` : ""}`;
350
+ const line = `${prefix} ${accent("|")} ${text(model)}${effort ? ` ${text(effort)}` : ""}${jev ? ` ${jev}` : ""} ${accent(sep)} ${contextText}${autoHandoff ? ` ${accent(sep)} ${autoHandoff}` : ""}${usageText}${git ? ` ${accent(sep)} ${text("git")} ${text(git)}` : ""}`;
337
351
  process.stdout.write(`${line}\n`);
338
352
 
339
353
  const lastMessage = latestUserMessage(status.transcript_path);
@@ -344,6 +358,6 @@ const main = async () => {
344
358
  };
345
359
 
346
360
  main().catch((error) => {
347
- process.stdout.write(`[🔥LITCLAUDE v${readVersion()}] | HUD unavailable: ${error.message}\n`);
361
+ process.stdout.write(`[🔥LIT v${readVersion()}] | HUD unavailable: ${error.message}\n`);
348
362
  process.exit(0);
349
363
  });
@@ -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.21";
12
+ const serverVersion = "1.0.23";
13
13
 
14
14
  const publicSourceReadTool = {
15
15
  name: "public_source_read",
@@ -1,4 +1,5 @@
1
1
  {
2
+ "modules": ["./lit-mod.mjs"],
2
3
  "hooks": {
3
4
  "SessionStart": [
4
5
  {
@@ -0,0 +1,44 @@
1
+ // The LIT band's surface module: the `Client` element lit-mod.mjs draws above the prompt. It runs on
2
+ // the drawing thread with no mods API, keeps the band's timeline in its local state, and animates on
3
+ // the surface's own frame clock only while something moves; once the timeline settles it stops its
4
+ // timer and the band stays still until the hooks module hands it a new model.
5
+
6
+ import { composeBand } from "../lib/mod/frame.mjs";
7
+ import { TIMINGS, motionProfile } from "../lib/mod/motion.mjs";
8
+ import { renderBand } from "../lib/mod/render.mjs";
9
+ import { advanceTimeline, createTimeline, sampleTimeline, timelineBusy } from "../lib/mod/timeline.mjs";
10
+
11
+ // The timeline lives in one holder object that the frame timer and every draw share. The timer
12
+ // never reads `surface.state` back, so a draw that folded in a new model is never overwritten by a
13
+ // tick that started from an older copy.
14
+ export default function LitBand(props, surface) {
15
+ const profile = motionProfile(props?.motion);
16
+ const model = props?.model;
17
+ const empty = () => surface.elements.Box({ children: [] });
18
+ if (!model) return empty();
19
+
20
+ const holder = surface.state?.live ?? { now: 0, model: null, timeline: null, stop: null };
21
+ if (holder.timeline === null) {
22
+ holder.timeline = createTimeline(model, 0, profile);
23
+ holder.model = model;
24
+ } else if (holder.model !== model) {
25
+ holder.timeline = advanceTimeline(holder.timeline, model, holder.now, profile);
26
+ holder.model = model;
27
+ }
28
+
29
+ if (holder.stop === null && timelineBusy(holder.timeline, holder.now, profile)) {
30
+ holder.stop = surface.every(TIMINGS.frame, () => {
31
+ holder.now += TIMINGS.frame;
32
+ if (!timelineBusy(holder.timeline, holder.now, profile)) {
33
+ holder.stop?.();
34
+ holder.stop = null;
35
+ }
36
+ surface.setState({ live: holder, now: holder.now });
37
+ });
38
+ }
39
+ if (surface.state?.live !== holder) surface.setState({ live: holder, now: holder.now });
40
+
41
+ if (!model.visible && !timelineBusy(holder.timeline, holder.now, profile)) return empty();
42
+ const frame = composeBand(model, sampleTimeline(holder.timeline, holder.now, profile), { columns: props.columns, glyphs: props.glyphs, accent: props.accent });
43
+ return renderBand(surface.elements, frame);
44
+ }