@litfamily/litclaude 1.0.21 → 1.0.22
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 +12 -0
- package/README.md +34 -92
- package/README_ko-KR.md +42 -95
- package/bin/icon-font.mjs +264 -0
- package/bin/litclaude-ai.js +155 -18
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +100 -5
- package/docs/privacy.md +2 -2
- package/package.json +13 -3
- package/plugins/litclaude/.claude-plugin/plugin.json +22 -2
- package/plugins/litclaude/bin/litclaude-hook.js +13 -7
- package/plugins/litclaude/bin/litclaude-hud.js +36 -19
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/hooks/hooks.json +1 -0
- package/plugins/litclaude/hooks/lit-band-client.mjs +44 -0
- package/plugins/litclaude/hooks/lit-mod.mjs +269 -0
- package/plugins/litclaude/lib/auto-handoff.mjs +2 -0
- package/plugins/litclaude/lib/automatic-update-refresh.mjs +4 -0
- package/plugins/litclaude/lib/automatic-update.mjs +148 -1
- package/plugins/litclaude/lib/cache-measurement.mjs +62 -1
- package/plugins/litclaude/lib/canonical-skill-resources.mjs +9 -9
- package/plugins/litclaude/lib/hud-accent-palette.mjs +2 -2
- package/plugins/litclaude/lib/interface-probe-gate.mjs +2 -1
- package/plugins/litclaude/lib/jev-skill-hint.mjs +2 -0
- package/plugins/litclaude/lib/lit-plan-persistence.mjs +2 -1
- package/plugins/litclaude/lib/litgoal/autoloop.mjs +2 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +2 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +3 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +4 -0
- package/plugins/litclaude/lib/mod/frame.mjs +378 -0
- package/plugins/litclaude/lib/mod/layout.mjs +76 -0
- package/plugins/litclaude/lib/mod/motion.mjs +101 -0
- package/plugins/litclaude/lib/mod/render.mjs +24 -0
- package/plugins/litclaude/lib/mod/settings.mjs +57 -0
- package/plugins/litclaude/lib/mod/state.mjs +374 -0
- package/plugins/litclaude/lib/mod/timeline.mjs +124 -0
- package/plugins/litclaude/lib/mod/view.mjs +232 -0
- package/plugins/litclaude/lib/motion-render-gate.mjs +2 -1
- package/plugins/litclaude/lib/office-runtime.mjs +6 -0
- package/plugins/litclaude/lib/project-state-root.mjs +34 -2
- package/plugins/litclaude/lib/rules/engine.mjs +2 -1
- package/plugins/litclaude/lib/rules/format.mjs +24 -3
- package/plugins/litclaude/lib/rules/session-state.mjs +2 -1
- package/plugins/litclaude/lib/secure-path-read.mjs +46 -0
- package/plugins/litclaude/lib/start-work-lifecycle.mjs +4 -0
- package/plugins/litclaude/lib/wikify-knowledge.mjs +2 -1
- package/plugins/litclaude/scripts/scaffold-plan.mjs +2 -1
- package/plugins/litclaude/skills/lit-team/scripts/team.mjs +2 -1
- 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
|
-
|
|
186
|
-
|
|
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
|
|
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,82 @@ 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
|
+
While LitClaude work is active in the session (a start-work plan, an open litgoal goal, a lit
|
|
350
|
+
discipline on a running turn, or live subagents) a band sits above the prompt:
|
|
351
|
+
|
|
352
|
+
- A ribbon of rounded pills, `LIT · PLAN · WORK n/m · REVIEW`, then a pink-to-cyan progress bar
|
|
353
|
+
whose leading cell fades in as it fills, the next task, the number of lanes, and the autoloop
|
|
354
|
+
count with the time left in its 30-minute budget. It is fitted to the terminal width, with Hangul
|
|
355
|
+
and emoji counted as two cells; parts drop in a fixed order when the row is narrow.
|
|
356
|
+
- A card with a round border when two or more lanes run or a warning needs a row: the phase
|
|
357
|
+
line, the next task, each lane with its state and elapsed time, and the gates (plan saved,
|
|
358
|
+
interface probe, motion gate, auto-handoff).
|
|
359
|
+
- Amber when something needs attention: a lane quiet for more than 10 minutes, or context within
|
|
360
|
+
five points of the auto-handoff percent (the WORK pill crossfades to amber and the ribbon shows
|
|
361
|
+
`ctx 76% → 80%`). A failed interface probe adds a red row and turns the border red.
|
|
362
|
+
- A notice row and a toast when a gate fires: lit-plan not saved, interface probe pending or
|
|
363
|
+
failed, motion gate, context near the auto-handoff percent, auto-handoff fired.
|
|
364
|
+
- A completion moment when the last task is checked, the work completes or the goal is met: a
|
|
365
|
+
light passes over the brand, embers scatter, and `COMPLETE` shows for about a second.
|
|
366
|
+
|
|
367
|
+
The spinner gains ` · <discipline> · n/m` after its ellipsis. `/lit-evidence` opens a pane with
|
|
368
|
+
three tabs (keys `1`, `2`, `3`): the goal's criteria with their last evidence, blockers and the
|
|
369
|
+
newest litgoal ledger entries; the lanes; and the gates. When the session is idle the band is not
|
|
370
|
+
drawn at all.
|
|
371
|
+
|
|
372
|
+
Motion: entrances pop in 60 ms apart with a slight overshoot (160 ms each), fills and colour
|
|
373
|
+
changes ease in and out (300 ms and 140 ms), notices slide in over 220 ms and leave over 150 ms,
|
|
374
|
+
and the finish lasts at most 1.2 s. The band's `Client` element animates on Claude Code's frame
|
|
375
|
+
clock only while something moves and stops its timer as soon as everything settles. In Claude
|
|
376
|
+
Code 2.1.292 a terminal row whose only change is a non-ASCII glyph is not repainted, so every
|
|
377
|
+
moving part changes ASCII text or colour: bar cells are coloured spaces, embers are ASCII sparks,
|
|
378
|
+
and a glyph swaps only together with its words.
|
|
379
|
+
|
|
380
|
+
Options, in `/config` under LitClaude (stored in `pluginConfigs`):
|
|
381
|
+
|
|
382
|
+
| Option | Values | Default |
|
|
383
|
+
| --- | --- | --- |
|
|
384
|
+
| `motion` | `full`, `subtle` (no overshoot, shimmer or embers, shorter), `off` (final frames only) | `full` |
|
|
385
|
+
| `glyphs` | `auto` (Nerd glyphs when the icon font is installed), `nerd`, or `basic` | `auto` |
|
|
386
|
+
| `band` | `auto` (only while work is active), `always`, `off` | `auto` |
|
|
387
|
+
|
|
388
|
+
Unknown values fall back to the default. The fields are plain strings with no `options` list on
|
|
389
|
+
purpose: Claude Code 2.1.270 and older refuse to load a plugin whose `userConfig` has one, and
|
|
390
|
+
that would take every LitClaude hook down with it. Nerd glyphs reach only the terminal; the
|
|
391
|
+
desktop app and the other surfaces always get the basic set.
|
|
392
|
+
|
|
393
|
+
The rounded pill caps (U+E0B6, U+E0B4) and icons come from a Nerd Font. `litclaude-ai install
|
|
394
|
+
--icon-font`, or a Yes to the install question (default No), adds the symbols-only Nerd Font,
|
|
395
|
+
Symbols Nerd Font Mono (MIT), to the per-user font folder (`~/Library/Fonts` on macOS,
|
|
396
|
+
`~/.local/share/fonts` on Linux, then `fc-cache -f` if present; on Windows it prints how to
|
|
397
|
+
install it by hand). Your terminal font stays as it is: most macOS terminals take the missing
|
|
398
|
+
icons from the installed font through system font fallback, and the installer prints one line for
|
|
399
|
+
your terminal (from `TERM_PROGRAM`) in case it does not. The archive is the pinned Nerd Fonts
|
|
400
|
+
release asset, refused unless its SHA-256 matches; `LITCLAUDE_ICON_FONT_SOURCE` names an offline
|
|
401
|
+
copy of the same archive. The licence goes next to the font, a manifest under `~/.litclaude`
|
|
402
|
+
records the two files, `uninstall` removes only those (and only while unchanged), and `doctor`
|
|
403
|
+
prints `ICON_FONT:`. With `glyphs` on `auto`, the band checks those font folders by stat at session
|
|
404
|
+
start and says what it decided on the `/lit-evidence` gates tab.
|
|
405
|
+
|
|
406
|
+
The mod is read-only. It reads LitClaude's own state and nothing else: the ignition record in
|
|
407
|
+
the per-user temp folder, `.litclaude/boulder.json` (found the way the hooks find it, across a
|
|
408
|
+
linked worktree), and under the outermost Git root's `.litclaude/` the litgoal goal, autoloop
|
|
409
|
+
counter and ledger, the lit-plan, interface-probe and motion turn records, and the auto-handoff
|
|
410
|
+
settings and session record. Each file must be a regular file under its size cap that resolves
|
|
411
|
+
exactly where its path says, so a symlinked file or state folder is skipped. It writes nothing,
|
|
412
|
+
starts no process, uses no network and calls no model. Its pure parts live in
|
|
413
|
+
`plugins/litclaude/lib/mod/` and are tested under Node.
|
|
414
|
+
|
|
415
|
+
On Claude Code releases before 2.1.287 the `modules` key is ignored and every settings hook runs
|
|
416
|
+
as before; there is no band.
|
|
417
|
+
|
|
323
418
|
## Bounded-authority start-work lifecycle
|
|
324
419
|
|
|
325
420
|
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
|
|
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.
|
|
4
|
-
"description": "Claude Code
|
|
3
|
+
"version": "1.0.22",
|
|
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
|
-
"
|
|
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.
|
|
4
|
+
"version": "1.0.22",
|
|
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 band shows above the prompt: auto (only while LitClaude work is active), always or off.",
|
|
34
|
+
"default": "auto"
|
|
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
|
|
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
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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 {
|
|
17
|
-
import { readIgnitionState, renderIgnitionSegment } from "../lib/hud-ignition.mjs";
|
|
16
|
+
import { latestUsageTokensCached } from "../lib/cache-measurement.mjs";
|
|
17
|
+
import { ignitionStatePath, readIgnitionState, renderIgnitionSegment } 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
|
-
|
|
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
|
|
231
|
-
if (hours > 0) return
|
|
232
|
-
return
|
|
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("]")}
|
|
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("]")}
|
|
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
|
|
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
|
|
320
|
-
const
|
|
321
|
-
|
|
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,17 @@ const main = async () => {
|
|
|
327
340
|
depth: colorDepth,
|
|
328
341
|
appearance,
|
|
329
342
|
});
|
|
330
|
-
const
|
|
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}`)}`;
|
|
331
347
|
const ignition = renderIgnitionSegment(readIgnitionState(status.session_id), {
|
|
332
348
|
depth: plainOutput ? "plain" : colorDepth,
|
|
333
349
|
});
|
|
350
|
+
const effort = effortLabel(status);
|
|
334
351
|
const jev = jevBadge(status);
|
|
335
352
|
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)}` : ""}`;
|
|
353
|
+
const line = `${prefix}${ignition ? ` ${ignition}` : ""} ${accent("|")} ${text(model)}${effort ? ` ${text(effort)}` : ""}${jev ? ` ${jev}` : ""} ${accent(sep)} ${contextText}${autoHandoff ? ` ${accent(sep)} ${autoHandoff}` : ""}${usageText}${git ? ` ${accent(sep)} ${text("git")} ${text(git)}` : ""}`;
|
|
337
354
|
process.stdout.write(`${line}\n`);
|
|
338
355
|
|
|
339
356
|
const lastMessage = latestUserMessage(status.transcript_path);
|
|
@@ -344,6 +361,6 @@ const main = async () => {
|
|
|
344
361
|
};
|
|
345
362
|
|
|
346
363
|
main().catch((error) => {
|
|
347
|
-
process.stdout.write(`[🔥
|
|
364
|
+
process.stdout.write(`[🔥LIT v${readVersion()}] | HUD unavailable: ${error.message}\n`);
|
|
348
365
|
process.exit(0);
|
|
349
366
|
});
|
|
@@ -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 });
|
|
43
|
+
return renderBand(surface.elements, frame);
|
|
44
|
+
}
|