@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
@@ -0,0 +1,53 @@
1
+ // Terminal cell arithmetic for the LIT band. The band is laid out by LitClaude itself rather than
2
+ // left to wrap, so every width here is in terminal cells: Hangul, CJK and emoji take two, combining
3
+ // marks and variation selectors none. Pure functions only; the mod runs them inside Claude Code and
4
+ // the tests run them under Node.
5
+
6
+ const ZERO_WIDTH = /[\p{Mn}\p{Me}​-‏︀-️]/u;
7
+ const EMOJI = /\p{Emoji_Presentation}/u;
8
+
9
+ const isWide = (code) =>
10
+ (code >= 0x1100 && code <= 0x115f)
11
+ || (code >= 0x2e80 && code <= 0x303e)
12
+ || (code >= 0x3041 && code <= 0x33ff)
13
+ || (code >= 0x3400 && code <= 0x4dbf)
14
+ || (code >= 0x4e00 && code <= 0x9fff)
15
+ || (code >= 0xa000 && code <= 0xa4cf)
16
+ || (code >= 0xa960 && code <= 0xa97f)
17
+ || (code >= 0xac00 && code <= 0xd7a3)
18
+ || (code >= 0xf900 && code <= 0xfaff)
19
+ || (code >= 0xfe30 && code <= 0xfe4f)
20
+ || (code >= 0xff00 && code <= 0xff60)
21
+ || (code >= 0xffe0 && code <= 0xffe6)
22
+ || (code >= 0x20000 && code <= 0x3fffd);
23
+
24
+ export const charWidth = (ch) => {
25
+ const code = ch.codePointAt(0) ?? 0;
26
+ if (code < 0x20 || (code >= 0x7f && code < 0xa0)) return 0;
27
+ if (ZERO_WIDTH.test(ch)) return 0;
28
+ if (EMOJI.test(ch) || isWide(code)) return 2;
29
+ return 1;
30
+ };
31
+
32
+ export const textWidth = (text) => {
33
+ let width = 0;
34
+ for (const ch of String(text)) width += charWidth(ch);
35
+ return width;
36
+ };
37
+
38
+ export const truncateToWidth = (text, cells, ellipsis = "…") => {
39
+ const value = String(text);
40
+ if (cells <= 0) return "";
41
+ if (textWidth(value) <= cells) return value;
42
+ const room = cells - textWidth(ellipsis);
43
+ if (room <= 0) return ellipsis.slice(0, cells);
44
+ let out = "";
45
+ let width = 0;
46
+ for (const ch of value) {
47
+ const w = charWidth(ch);
48
+ if (width + w > room) break;
49
+ out += ch;
50
+ width += w;
51
+ }
52
+ return `${out.trimEnd()}${ellipsis}`;
53
+ };
@@ -0,0 +1,101 @@
1
+ // Motion vocabulary for the LIT band: easing curves, the approved timings, animated channels and
2
+ // colour mixing. Entrances pop with ease-out-back, fills and colour changes run ease-in-out, exits
3
+ // accelerate with ease-in. A channel is plain data ({ from, to, at, dur }) sampled against the
4
+ // surface's frame clock, so the band module keeps no timers beyond the frames it is animating.
5
+
6
+ export const EASE = Object.freeze({
7
+ linear: (x) => x,
8
+ outCubic: (x) => 1 - (1 - x) ** 3,
9
+ inCubic: (x) => x ** 3,
10
+ inOutCubic: (x) => (x < 0.5 ? 4 * x ** 3 : 1 - (-2 * x + 2) ** 3 / 2),
11
+ outBack: (x) => 1 + 2.7 * (x - 1) ** 3 + 1.7 * (x - 1) ** 2,
12
+ });
13
+
14
+ // Milliseconds. The user approved this faster set in the motion preview; `completion` bounds the
15
+ // whole finish moment (shimmer plus embers) and `frame` is the redraw interval while animating.
16
+ export const TIMINGS = Object.freeze({
17
+ stagger: 60,
18
+ entry: 160,
19
+ fill: 300,
20
+ flash: 400,
21
+ noticeIn: 220,
22
+ noticeOut: 150,
23
+ noticeHold: 4000,
24
+ crossfade: 140,
25
+ shimmer: 400,
26
+ embers: 1000,
27
+ completion: 1200,
28
+ exit: 150,
29
+ frame: 33,
30
+ });
31
+
32
+ // full: the approved motion. subtle: no overshoot, no shimmer or embers, three quarters the time.
33
+ // off: every duration is zero, so the band draws its final frame and never starts a frame timer.
34
+ export const motionProfile = (mode) => {
35
+ if (mode === "off") {
36
+ return { mode, duration: () => 0, enterEase: EASE.linear, noticeEase: EASE.linear, shimmer: false, embers: false };
37
+ }
38
+ if (mode === "subtle") {
39
+ return { mode, duration: (ms) => Math.round(ms * 0.75), enterEase: EASE.outCubic, noticeEase: EASE.outCubic, shimmer: false, embers: false };
40
+ }
41
+ return { mode: "full", duration: (ms) => ms, enterEase: EASE.outBack, noticeEase: EASE.outBack, shimmer: true, embers: true };
42
+ };
43
+
44
+ export const clamp01 = (x) => (x <= 0 ? 0 : x >= 1 ? 1 : x);
45
+
46
+ // Progress through a window that opens at `at` and lasts `dur`; a zero-length window is complete.
47
+ export const phase = (now, at, dur) => (dur <= 0 ? (now >= at ? 1 : 0) : clamp01((now - at) / dur));
48
+
49
+ export const still = (value) => ({ from: value, to: value, at: 0, dur: 0 });
50
+
51
+ export const sample = (channel, now, ease = EASE.inOutCubic) =>
52
+ channel.from + (channel.to - channel.from) * ease(phase(now, channel.at, channel.dur));
53
+
54
+ export const retarget = (channel, to, now, dur) => {
55
+ if (channel.to === to) return channel;
56
+ const from = sample(channel, now);
57
+ return from === to ? still(to) : { from, to, at: now, dur };
58
+ };
59
+
60
+ export const isSettled = (channel, now) => channel.from === channel.to || now >= channel.at + channel.dur;
61
+
62
+ const hexToRgb = (hex) => {
63
+ const value = String(hex).replace(/^#/u, "");
64
+ return [0, 2, 4].map((i) => Number.parseInt(value.slice(i, i + 2), 16) || 0);
65
+ };
66
+
67
+ export const lerpColor = (a, b, t) => {
68
+ const from = hexToRgb(a);
69
+ const to = hexToRgb(b);
70
+ const k = clamp01(t);
71
+ return `#${from.map((v, i) => Math.round(v + (to[i] - v) * k).toString(16).padStart(2, "0")).join("")}`;
72
+ };
73
+
74
+ export const gradientAt = (stops, position) => {
75
+ if (stops.length === 1) return lerpColor(stops[0], stops[0], 0);
76
+ const scaled = clamp01(position) * (stops.length - 1);
77
+ const index = Math.min(stops.length - 2, Math.floor(scaled));
78
+ return lerpColor(stops[index], stops[index + 1], scaled - index);
79
+ };
80
+
81
+ export const BAR_STOPS = Object.freeze(["#ff2d95", "#00e5ff"]);
82
+ export const BAR_TRACK = "#30363d";
83
+
84
+ // A bar of `cells` cells filled to `fraction`. Whole cells take the pink-to-cyan gradient; the
85
+ // cell the fill is inside fades from the track colour to its gradient colour by how far it is
86
+ // filled, so the bar moves continuously in colour rather than in eighth-cell glyphs (a glyph-only
87
+ // change is not repainted inside a Claude Code Client region). While the bar moves, `glow` (0 to 1)
88
+ // brightens that leading cell. Every filled or fading cell is a solid `█`; the rest is the track.
89
+ export const barCells = (fraction, cells, glow = 0) => {
90
+ const filled = clamp01(fraction) * cells;
91
+ const full = Math.floor(filled + 1e-9);
92
+ const part = full < cells ? filled - full : 0;
93
+ const head = part > 1e-6 ? full : full - 1;
94
+ const gradient = (i) => gradientAt(BAR_STOPS, cells <= 1 ? 0 : i / (cells - 1));
95
+ const shine = (i, color) => (i === head && glow > 0 ? lerpColor(color, "#ffffff", glow * 0.7) : color);
96
+ return Array.from({ length: cells }, (_, i) => {
97
+ if (i < full) return { glyph: "█", color: shine(i, gradient(i)) };
98
+ if (i === full && part > 1e-6) return { glyph: "█", color: shine(i, lerpColor(BAR_TRACK, gradient(i), part)) };
99
+ return { glyph: "░", color: BAR_TRACK };
100
+ });
101
+ };
@@ -0,0 +1,20 @@
1
+ // Draws composed rows (frame.mjs) with a surface's element table: one row Box of Text spans per
2
+ // line. The LIT card draws its own border as text, so its title and figures sit in the edges. It
3
+ // calls the element constructors directly, so the band's surface module, the mod's static fallback
4
+ // and the tests share it.
5
+
6
+ const textProps = (part) => {
7
+ const props = { children: part.text, wrap: "truncate-end" };
8
+ if (part.fg) props.color = part.fg;
9
+ if (part.bg) props.backgroundColor = part.bg;
10
+ if (part.bold) props.bold = true;
11
+ return props;
12
+ };
13
+
14
+ export const renderRows = ({ Box, Text }, rows) =>
15
+ rows.map((row) => {
16
+ const parts = row.filter(({ text }) => text.length > 0);
17
+ return Box({ flexDirection: "row", children: parts.length ? parts.map((part) => Text(textProps(part))) : [Text({ children: " " })] });
18
+ });
19
+
20
+ export const renderBand = (elements, frame) => elements.Box({ flexDirection: "column", children: renderRows(elements, frame.rows) });
@@ -0,0 +1,105 @@
1
+ // The LIT band's three userConfig options. The manifest declares them as plain strings without an
2
+ // `options` list, because Claude Code 2.1.270 and older refuse a whole plugin whose userConfig has
3
+ // one; the values are checked here instead, and anything unknown falls back to the default (the
4
+ // first choice).
5
+
6
+ const CHOICES = Object.freeze({
7
+ motion: ["full", "subtle", "off"],
8
+ glyphs: ["auto", "nerd", "basic"],
9
+ band: ["always", "auto", "off"],
10
+ });
11
+
12
+ const pick = (value, choices) => {
13
+ const normalized = String(value ?? "").trim().toLowerCase();
14
+ return choices.includes(normalized) ? normalized : choices[0];
15
+ };
16
+
17
+ export const normalizeModSettings = (options) => ({
18
+ motion: pick(options?.motion, CHOICES.motion),
19
+ glyphs: pick(options?.glyphs, CHOICES.glyphs),
20
+ band: pick(options?.band, CHOICES.band),
21
+ });
22
+
23
+ // The glyph set one surface draws. Private-use Nerd Font glyphs only reach the terminal, which
24
+ // draws with the person's own fonts; `auto` uses them there when the icon font is installed. The
25
+ // desktop app and the other surfaces always get the basic set.
26
+ export const resolveGlyphs = (preference, surface, iconFontPresent) => {
27
+ if (surface !== "terminal") return "basic";
28
+ if (preference === "auto") return iconFontPresent ? "nerd" : "basic";
29
+ return preference === "nerd" ? "nerd" : "basic";
30
+ };
31
+
32
+ const BASIC = Object.freeze({
33
+ capLeft: "",
34
+ capRight: "",
35
+ done: "✓",
36
+ now: "●",
37
+ warn: "⚠",
38
+ link: "─",
39
+ rail: "━",
40
+ next: "▸",
41
+ lane: "●",
42
+ idleLane: "◌",
43
+ loop: "⟳",
44
+ fail: "✗",
45
+ flag: "⚑",
46
+ bolt: "⚡",
47
+ recentBolt: "↯",
48
+ branch: "git",
49
+ });
50
+
51
+ const NERD = Object.freeze({
52
+ ...BASIC,
53
+ capLeft: "",
54
+ capRight: "",
55
+ done: "",
56
+ warn: "",
57
+ bolt: "",
58
+ recentBolt: "",
59
+ branch: "",
60
+ });
61
+
62
+ export const glyphSet = (glyphs) => (glyphs === "nerd" ? NERD : BASIC);
63
+
64
+ // The status line's accents (hud-accent-palette.mjs HUD_ACCENT_THEMES, kept equal by a test) as
65
+ // xterm 256-colour codes. The mod cannot import that file, so the card reads the same names here.
66
+ export const ACCENT_CODES = Object.freeze({
67
+ cyan: 81,
68
+ blue: 39,
69
+ teal: 49,
70
+ green: 118,
71
+ lavender: 177,
72
+ rose: 198,
73
+ gold: 220,
74
+ orange: 208,
75
+ slate: 75,
76
+ gray: 231,
77
+ });
78
+
79
+ const CUBE = [0, 95, 135, 175, 215, 255];
80
+ const hex2 = (value) => value.toString(16).padStart(2, "0");
81
+
82
+ export const xterm256Hex = (code) => {
83
+ if (code >= 232) {
84
+ const level = 8 + (code - 232) * 10;
85
+ return `#${hex2(level).repeat(3)}`;
86
+ }
87
+ const index = code - 16;
88
+ return `#${[Math.floor(index / 36), Math.floor(index / 6) % 6, index % 6].map((step) => hex2(CUBE[step])).join("")}`;
89
+ };
90
+
91
+ export const accentHex = (name) => xterm256Hex(ACCENT_CODES[String(name ?? "").trim().toLowerCase()] ?? ACCENT_CODES.cyan);
92
+
93
+ const knownAccent = (name) => {
94
+ const normalized = String(name ?? "").trim().toLowerCase();
95
+ return Object.hasOwn(ACCENT_CODES, normalized) ? normalized : null;
96
+ };
97
+
98
+ // The accent the status line draws with: the installer writes it into the statusLine command
99
+ // (`LITCLAUDE_HUD_ACCENT=<name> node …`) and records it under the `litclaude` settings key; an
100
+ // accent exported in the shell is the last resort, as for a hand-written status line.
101
+ export const accentFromSettings = (settings, envValue) => {
102
+ const command = typeof settings?.statusLine?.command === "string" ? settings.statusLine.command : "";
103
+ const inline = /\bLITCLAUDE_HUD_ACCENT=([A-Za-z]+)/u.exec(command)?.[1];
104
+ return accentHex(knownAccent(inline) ?? knownAccent(settings?.litclaude?.hudAccent) ?? knownAccent(envValue) ?? "cyan");
105
+ };
@@ -0,0 +1,81 @@
1
+ // Which LitFamily skills fired in this session, for the chips row of the LIT card. The mod notes a
2
+ // firing from a Skill tool call, a skill prompt expansion (a typed `/litclaude:<name>` or the Skill
3
+ // tool) and a new ignition record from the prompt hook (bare `lit` routing); only LitClaude's own
4
+ // skills and commands become chips. Pure: the mod keeps the list, the card draws it.
5
+
6
+ // canonical-skill-catalog.mjs canonicalSkillIds, kept equal by a test; the catalog pulls in the
7
+ // resource manifest, which the mod has no reason to load.
8
+ export const LIT_SKILL_IDS = Object.freeze([
9
+ "autoconference",
10
+ "autoresearch",
11
+ "browser-drive",
12
+ "comment-checker",
13
+ "debugging",
14
+ "deep-interview",
15
+ "frontend-ui-ux",
16
+ "lit-burnoff",
17
+ "lit-burnoff-file",
18
+ "lit-code",
19
+ "lit-commit",
20
+ "lit-comprehend",
21
+ "lit-crucible",
22
+ "lit-diagram-drawer",
23
+ "lit-docx",
24
+ "lit-handoff",
25
+ "lit-humanizer",
26
+ "lit-init",
27
+ "lit-loop",
28
+ "lit-plan",
29
+ "lit-pptx",
30
+ "lit-recap",
31
+ "lit-scientific-visualization",
32
+ "lit-team",
33
+ "lit-typographic-motion",
34
+ "litgoal",
35
+ "litresearch",
36
+ "litwork",
37
+ "lsp",
38
+ "lsp-setup",
39
+ "readme-studio",
40
+ "refactor",
41
+ "review-work",
42
+ "rules",
43
+ "start-work",
44
+ "structural-search",
45
+ "visual-qa",
46
+ "wikify",
47
+ ]);
48
+
49
+ const KNOWN = new Set(LIT_SKILL_IDS);
50
+ const PLUGIN_PREFIX = "litclaude:";
51
+ const KEEP = 12;
52
+ // A Skill call and the prompt expansion it causes arrive moments apart; they are one firing.
53
+ const SAME_FIRING_MS = 1500;
54
+
55
+ /** The chip id for a skill or command name, or null when it is not LitClaude's. */
56
+ export const litSkillId = (name) => {
57
+ const raw = typeof name === "string" ? name.trim() : "";
58
+ if (raw.startsWith(PLUGIN_PREFIX)) {
59
+ const id = raw.slice(PLUGIN_PREFIX.length);
60
+ return /^[a-z0-9][a-z0-9-]{0,63}$/u.test(id) ? id : null;
61
+ }
62
+ return KNOWN.has(raw) ? raw : null;
63
+ };
64
+
65
+ /** Notes one firing at `at`: newest first, one entry per skill, a short history. */
66
+ export const noteSkill = (fired, id, at) => {
67
+ const previous = fired.find((entry) => entry.id === id);
68
+ if (previous && at >= previous.at && at - previous.at < SAME_FIRING_MS) return fired;
69
+ return [{ id, at }, ...fired.filter((entry) => entry.id !== id)].slice(0, KEEP);
70
+ };
71
+
72
+ /**
73
+ * Each chip's state: `current` when it fired after `settledAt` (the end of the last finished
74
+ * request), `recent` otherwise. Current chips come first; each group keeps newest first.
75
+ */
76
+ export const chipStates = (fired, settledAt) => {
77
+ const ordered = [...fired].sort((a, b) => b.at - a.at);
78
+ const current = ordered.filter(({ at }) => at > settledAt).map((entry) => ({ ...entry, state: "current" }));
79
+ const recent = ordered.filter(({ at }) => at <= settledAt).map((entry) => ({ ...entry, state: "recent" }));
80
+ return [...current, ...recent];
81
+ };