@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.
- package/CHANGELOG.md +18 -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 +131 -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 -22
- 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 +314 -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/hud-ignition.mjs +7 -37
- 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 +454 -0
- package/plugins/litclaude/lib/mod/layout.mjs +53 -0
- package/plugins/litclaude/lib/mod/motion.mjs +101 -0
- package/plugins/litclaude/lib/mod/render.mjs +20 -0
- package/plugins/litclaude/lib/mod/settings.mjs +105 -0
- package/plugins/litclaude/lib/mod/skills.mjs +81 -0
- package/plugins/litclaude/lib/mod/state.mjs +411 -0
- package/plugins/litclaude/lib/mod/timeline.mjs +152 -0
- package/plugins/litclaude/lib/mod/view.mjs +255 -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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// LitClaude's Claude Code mod (hooks/hooks.json `modules`). It draws the LIT card above the prompt
|
|
2
|
+
// for the whole session (the `band` setting can limit it to active work or turn it off), lights a
|
|
3
|
+
// chip for each LitFamily skill that fires, adds the discipline and progress to the spinner, raises
|
|
4
|
+
// gate notices, and serves /lit-evidence. It is read-only: it reads LitClaude's own state files and
|
|
5
|
+
// the Git HEAD through lib/mod/state.mjs, the settings for the status line's accent, and the host's
|
|
6
|
+
// live figures, and never writes, spawns, fetches or calls a model.
|
|
7
|
+
// Claude Code releases before mods ignore the `modules` key and keep running the settings hooks.
|
|
8
|
+
//
|
|
9
|
+
// The mods loader only follows `$` into functions declared at the top of this file, so each helper
|
|
10
|
+
// below takes `$` and the per-load `live` record explicitly instead of closing over them.
|
|
11
|
+
|
|
12
|
+
import { composeBand, composeEvidence } from "../lib/mod/frame.mjs";
|
|
13
|
+
import { TIMINGS } from "../lib/mod/motion.mjs";
|
|
14
|
+
import { renderBand, renderRows } from "../lib/mod/render.mjs";
|
|
15
|
+
import { accentFromSettings, normalizeModSettings, resolveGlyphs } from "../lib/mod/settings.mjs";
|
|
16
|
+
import { chipStates, litSkillId, noteSkill } from "../lib/mod/skills.mjs";
|
|
17
|
+
import { detectIconFont, readGitBranch, readLedgerTail, readLitSnapshot } from "../lib/mod/state.mjs";
|
|
18
|
+
import { settledSample } from "../lib/mod/timeline.mjs";
|
|
19
|
+
import {
|
|
20
|
+
EMPTY_ACTIVITY,
|
|
21
|
+
buildBandModel,
|
|
22
|
+
buildEvidenceModel,
|
|
23
|
+
detectCompletion,
|
|
24
|
+
diffNotices,
|
|
25
|
+
noteAgentActivity,
|
|
26
|
+
spinnerSuffix,
|
|
27
|
+
trackAgents,
|
|
28
|
+
} from "../lib/mod/view.mjs";
|
|
29
|
+
|
|
30
|
+
const PANE = "lit-evidence";
|
|
31
|
+
const POLL_MS = 2000;
|
|
32
|
+
const QUICK_REFRESH_MS = 250;
|
|
33
|
+
const TABS = Object.freeze([
|
|
34
|
+
{ id: "goal", label: "goal", hotkey: "1" },
|
|
35
|
+
{ id: "lanes", label: "lanes", hotkey: "2" },
|
|
36
|
+
{ id: "gates", label: "gates", hotkey: "3" },
|
|
37
|
+
]);
|
|
38
|
+
const NO_SNAPSHOT = Object.freeze({ ignition: null, work: null, completedWorkIds: [], goal: null, autoloop: null, litPlan: null, probe: null, motion: null, handoff: null });
|
|
39
|
+
|
|
40
|
+
const createLive = (options) => ({
|
|
41
|
+
band: normalizeModSettings(options).band,
|
|
42
|
+
iconFont: { present: false, path: null, note: "basic: not checked yet" },
|
|
43
|
+
env: {},
|
|
44
|
+
snapshot: null,
|
|
45
|
+
model: null,
|
|
46
|
+
agents: [],
|
|
47
|
+
usage: null,
|
|
48
|
+
activity: EMPTY_ACTIVITY,
|
|
49
|
+
statusById: {},
|
|
50
|
+
isWorking: false,
|
|
51
|
+
notice: null,
|
|
52
|
+
completion: null,
|
|
53
|
+
exiting: false,
|
|
54
|
+
clientFailed: false,
|
|
55
|
+
refreshing: false,
|
|
56
|
+
quickPending: false,
|
|
57
|
+
paneOpen: false,
|
|
58
|
+
evidenceTab: "goal",
|
|
59
|
+
ledger: [],
|
|
60
|
+
celebrated: new Set(),
|
|
61
|
+
accent: undefined,
|
|
62
|
+
git: null,
|
|
63
|
+
fired: [],
|
|
64
|
+
settledAt: 0,
|
|
65
|
+
ignitionAt: undefined,
|
|
66
|
+
lastDiscipline: null,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const stateIo = ($) => ({
|
|
70
|
+
stat: (path) => $.fs.stat(path, { resolve: true }),
|
|
71
|
+
read: (path) => $.fs.read(path),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const toastText = (raised) => `🔥 ${raised.title}${raised.detail ? ` · ${raised.detail}` : ""}`;
|
|
75
|
+
|
|
76
|
+
async function rebuild($, live) {
|
|
77
|
+
const now = await $.clock.now();
|
|
78
|
+
const previous = live.model;
|
|
79
|
+
const next = buildBandModel({
|
|
80
|
+
snapshot: live.snapshot ?? NO_SNAPSHOT,
|
|
81
|
+
agents: live.agents,
|
|
82
|
+
activity: live.activity,
|
|
83
|
+
usage: live.usage,
|
|
84
|
+
now,
|
|
85
|
+
band: live.band,
|
|
86
|
+
isWorking: live.isWorking,
|
|
87
|
+
notice: live.notice,
|
|
88
|
+
completion: live.completion,
|
|
89
|
+
skills: chipStates(live.fired, live.settledAt),
|
|
90
|
+
lastDiscipline: live.lastDiscipline,
|
|
91
|
+
git: live.git,
|
|
92
|
+
});
|
|
93
|
+
live.model = next;
|
|
94
|
+
if (previous?.visible && !next.visible && !live.exiting) {
|
|
95
|
+
// Keep the band mounted while its surface module plays the exit, then let it go.
|
|
96
|
+
live.exiting = true;
|
|
97
|
+
$.clock.after(TIMINGS.exit + 80, () => {
|
|
98
|
+
live.exiting = false;
|
|
99
|
+
$.ui.invalidate("ui.render");
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (JSON.stringify(previous) !== JSON.stringify(next)) $.ui.invalidate("ui.render");
|
|
103
|
+
return next;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function showNotice($, live, raised) {
|
|
107
|
+
live.notice = raised;
|
|
108
|
+
$.ui.toast(toastText(raised));
|
|
109
|
+
$.clock.after(TIMINGS.noticeHold, () => {
|
|
110
|
+
if (live.notice?.id !== raised.id) return;
|
|
111
|
+
live.notice = null;
|
|
112
|
+
void rebuild($, live);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function celebrate($, live, finish) {
|
|
117
|
+
if (live.celebrated.has(finish.id)) return;
|
|
118
|
+
live.celebrated.add(finish.id);
|
|
119
|
+
live.completion = finish;
|
|
120
|
+
$.clock.after(TIMINGS.completion + 200, () => {
|
|
121
|
+
if (live.completion?.id !== finish.id) return;
|
|
122
|
+
live.completion = null;
|
|
123
|
+
void rebuild($, live);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A firing of a skill or command by name; anything that is not LitClaude's is ignored.
|
|
128
|
+
async function noteFiring($, live, name) {
|
|
129
|
+
const id = litSkillId(name);
|
|
130
|
+
if (!id) return;
|
|
131
|
+
live.fired = noteSkill(live.fired, id, await $.clock.now());
|
|
132
|
+
live.lastDiscipline = id;
|
|
133
|
+
await rebuild($, live);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The prompt hook rewrites the ignition record on every prompt; a new record naming a discipline is
|
|
137
|
+
// a bare `lit` (or slash) routing, and lights that skill's chip at the time the hook wrote it.
|
|
138
|
+
function noteIgnition(live, ignition, now) {
|
|
139
|
+
if (!ignition || ignition.at === live.ignitionAt) return;
|
|
140
|
+
live.ignitionAt = ignition.at;
|
|
141
|
+
live.lastDiscipline = ignition.discipline;
|
|
142
|
+
const id = litSkillId(ignition.discipline);
|
|
143
|
+
if (id) live.fired = noteSkill(live.fired, id, ignition.at ?? now);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function refresh($, live) {
|
|
147
|
+
if (live.refreshing) return;
|
|
148
|
+
live.refreshing = true;
|
|
149
|
+
try {
|
|
150
|
+
const [cwd, sessionId] = await Promise.all([$.session.cwd(), $.session.id()]);
|
|
151
|
+
const fresh = await readLitSnapshot(stateIo($), { cwd, sessionId, env: live.env });
|
|
152
|
+
live.git = await readGitBranch(stateIo($), cwd).catch(() => live.git);
|
|
153
|
+
live.agents = await $.agent.list().catch(() => []);
|
|
154
|
+
live.usage = await $.session.usage().catch(() => live.usage);
|
|
155
|
+
const now = await $.clock.now();
|
|
156
|
+
live.activity = trackAgents(live.activity, live.agents, live.statusById, now);
|
|
157
|
+
live.statusById = Object.fromEntries(live.agents.map((agent) => [agent.id, agent.status]));
|
|
158
|
+
const before = live.snapshot;
|
|
159
|
+
const previousModel = live.model;
|
|
160
|
+
live.snapshot = fresh;
|
|
161
|
+
noteIgnition(live, fresh.ignition, now);
|
|
162
|
+
const finish = detectCompletion(before, fresh);
|
|
163
|
+
if (finish) celebrate($, live, finish);
|
|
164
|
+
const next = await rebuild($, live);
|
|
165
|
+
const raised = diffNotices(before, fresh, previousModel, next);
|
|
166
|
+
if (raised.length > 0) {
|
|
167
|
+
for (const extra of raised.slice(0, -1)) $.ui.toast(toastText(extra));
|
|
168
|
+
showNotice($, live, raised.at(-1));
|
|
169
|
+
await rebuild($, live);
|
|
170
|
+
}
|
|
171
|
+
if (live.paneOpen) live.ledger = await readLedgerTail(stateIo($), fresh.stateRoot);
|
|
172
|
+
} catch {
|
|
173
|
+
// A failed read leaves the last good band in place; the next poll tries again.
|
|
174
|
+
} finally {
|
|
175
|
+
live.refreshing = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function refreshSoon($, live) {
|
|
180
|
+
if (live.quickPending) return;
|
|
181
|
+
live.quickPending = true;
|
|
182
|
+
$.clock.after(QUICK_REFRESH_MS, () => {
|
|
183
|
+
live.quickPending = false;
|
|
184
|
+
void refresh($, live);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const register = (on, options) => {
|
|
189
|
+
const live = createLive(options);
|
|
190
|
+
|
|
191
|
+
on("session.start", async ($, e, next) => {
|
|
192
|
+
live.env = {
|
|
193
|
+
TMPDIR: await $.env.get("TMPDIR"),
|
|
194
|
+
LITCLAUDE_HUD_STATE_ROOT: await $.env.get("LITCLAUDE_HUD_STATE_ROOT"),
|
|
195
|
+
LITCLAUDE_AUTO_HANDOFF: await $.env.get("LITCLAUDE_AUTO_HANDOFF"),
|
|
196
|
+
LITCLAUDE_AUTO_HANDOFF_PERCENT: await $.env.get("LITCLAUDE_AUTO_HANDOFF_PERCENT"),
|
|
197
|
+
};
|
|
198
|
+
live.iconFont = await detectIconFont(stateIo($), {
|
|
199
|
+
HOME: await $.env.get("HOME"),
|
|
200
|
+
XDG_DATA_HOME: await $.env.get("XDG_DATA_HOME"),
|
|
201
|
+
LOCALAPPDATA: await $.env.get("LOCALAPPDATA"),
|
|
202
|
+
});
|
|
203
|
+
live.settledAt = await $.clock.now();
|
|
204
|
+
live.accent = accentFromSettings(await $.settings.read().catch(() => null), await $.env.get("LITCLAUDE_HUD_ACCENT"));
|
|
205
|
+
await $.command.register({ name: PANE, description: "Show LitClaude goal criteria, evidence, lanes and gates in a pane" });
|
|
206
|
+
await refresh($, live);
|
|
207
|
+
$.clock.every(POLL_MS, () => {
|
|
208
|
+
void refresh($, live);
|
|
209
|
+
});
|
|
210
|
+
return next(e);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
on("turn.start", async ($, e, next) => {
|
|
214
|
+
live.isWorking = true;
|
|
215
|
+
refreshSoon($, live);
|
|
216
|
+
return next(e);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
on("turn.complete", async ($, e, next) => {
|
|
220
|
+
live.isWorking = false;
|
|
221
|
+
// The request is over: its chips dim to the recent list until a skill fires again.
|
|
222
|
+
live.settledAt = await $.clock.now();
|
|
223
|
+
refreshSoon($, live);
|
|
224
|
+
return next(e);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
on("session.measure", async ($, e, next) => {
|
|
228
|
+
live.usage = { context: e.context, rateLimits: e.rateLimits };
|
|
229
|
+
if (e.changed.includes("context")) refreshSoon($, live);
|
|
230
|
+
return next(e);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
on("tool.call", async ($, e, next) => {
|
|
234
|
+
if (e.tool === "Skill") await noteFiring($, live, e.skill);
|
|
235
|
+
const result = await next(e);
|
|
236
|
+
if (typeof e.agentId === "string") live.activity = noteAgentActivity(live.activity, e.agentId, await $.clock.now());
|
|
237
|
+
refreshSoon($, live);
|
|
238
|
+
return result;
|
|
239
|
+
}).catch(($, e, next) => next(e));
|
|
240
|
+
|
|
241
|
+
// A typed `/litclaude:<name>` and the Skill tool both expand the skill's prompt here; the text
|
|
242
|
+
// passes through untouched.
|
|
243
|
+
on("skill.prompt", async ($, e, next) => {
|
|
244
|
+
await noteFiring($, live, e.skill);
|
|
245
|
+
return next(e);
|
|
246
|
+
}).catch(($, e, next) => next(e));
|
|
247
|
+
|
|
248
|
+
on("ui.fault", async ($, e, next) => {
|
|
249
|
+
if (e.component === "AbovePrompt") live.clientFailed = true;
|
|
250
|
+
return next(e);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
|
|
254
|
+
const settings = normalizeModSettings(options);
|
|
255
|
+
const glyphs = resolveGlyphs(settings.glyphs, e.surface, live.iconFont.present);
|
|
256
|
+
const model = live.model;
|
|
257
|
+
if (!model || settings.band === "off" || e.props.hasSurvey || (!model.visible && !live.exiting)) return next(e);
|
|
258
|
+
const elements = $.ui.resolve(e);
|
|
259
|
+
const columns = Math.max(20, e.props.bodyColumns ?? e.viewport?.columns ?? 80);
|
|
260
|
+
let drawn = null;
|
|
261
|
+
if (elements.Client && !live.clientFailed) {
|
|
262
|
+
drawn = elements.Client({ key: "lit-band", module: "./lit-band-client.mjs", props: { model, columns, glyphs, motion: settings.motion, accent: live.accent } });
|
|
263
|
+
} else if (model.visible) {
|
|
264
|
+
drawn = renderBand(elements, composeBand(model, settledSample(model), { columns, glyphs, accent: live.accent }));
|
|
265
|
+
}
|
|
266
|
+
const below = await next(e);
|
|
267
|
+
if (!drawn) return below;
|
|
268
|
+
return below ? elements.Box({ flexDirection: "column", children: [drawn, below] }) : drawn;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
on("ui.render", { component: "Spinner" }, async ($, e, next) => {
|
|
272
|
+
const suffix = spinnerSuffix(live.model, e.props.suffix);
|
|
273
|
+
return suffix ? next({ ...e, props: { ...e.props, suffix } }) : next(e);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
on("command.run", { command: PANE }, async ($) => {
|
|
277
|
+
live.paneOpen = true;
|
|
278
|
+
if (live.snapshot) live.ledger = await readLedgerTail(stateIo($), live.snapshot.stateRoot);
|
|
279
|
+
await $.ui.open({ id: PANE, title: "LIT evidence", focus: true, closeOnEscape: true });
|
|
280
|
+
return {};
|
|
281
|
+
}).catch(() => ({ text: "The LIT evidence pane could not open; LitClaude state is unchanged." }));
|
|
282
|
+
|
|
283
|
+
on("ui.close", async ($, e, next) => {
|
|
284
|
+
if (e.id === PANE) live.paneOpen = false;
|
|
285
|
+
return next(e);
|
|
286
|
+
}).catch(($, e, next) => next(e));
|
|
287
|
+
|
|
288
|
+
on("ui.render", { component: "Pane", requestId: PANE }, async ($, e) => {
|
|
289
|
+
const settings = normalizeModSettings(options);
|
|
290
|
+
const glyphs = resolveGlyphs(settings.glyphs, e.surface, live.iconFont.present);
|
|
291
|
+
const elements = $.ui.resolve(e);
|
|
292
|
+
const columns = Math.max(20, e.props.bodyColumns ?? 60);
|
|
293
|
+
const now = await $.clock.now();
|
|
294
|
+
const evidence = buildEvidenceModel({ snapshot: live.snapshot ?? NO_SNAPSHOT, ledger: live.ledger, agents: live.agents, activity: live.activity, now, icons: `${glyphs} glyphs (${settings.glyphs}); ${live.iconFont.note}` });
|
|
295
|
+
const tabs = TABS.map((tab) => elements.Button({
|
|
296
|
+
key: `tab-${tab.id}`,
|
|
297
|
+
label: tab.label,
|
|
298
|
+
hotkey: tab.hotkey,
|
|
299
|
+
plain: true,
|
|
300
|
+
dimColor: tab.id !== live.evidenceTab,
|
|
301
|
+
onPress: () => {
|
|
302
|
+
live.evidenceTab = tab.id;
|
|
303
|
+
$.ui.invalidate("ui.render");
|
|
304
|
+
},
|
|
305
|
+
}));
|
|
306
|
+
return elements.Box({
|
|
307
|
+
flexDirection: "column",
|
|
308
|
+
children: [
|
|
309
|
+
elements.Box({ flexDirection: "row", gap: 2, children: tabs }),
|
|
310
|
+
...renderRows(elements, composeEvidence(evidence, live.evidenceTab, { columns, glyphs })),
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
};
|
|
@@ -16,6 +16,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
|
|
18
18
|
import { latestUsageTokens } from "./cache-measurement.mjs";
|
|
19
|
+
import { ensureStateDirIgnored } from "./project-state-root.mjs";
|
|
19
20
|
|
|
20
21
|
export const AUTO_HANDOFF_FLAG = "LITCLAUDE_AUTO_HANDOFF";
|
|
21
22
|
export const AUTO_HANDOFF_PERCENT = "LITCLAUDE_AUTO_HANDOFF_PERCENT";
|
|
@@ -62,6 +63,7 @@ const stateDirectory = (stateRoot, { create = false } = {}) => {
|
|
|
62
63
|
}
|
|
63
64
|
if (!stat.isDirectory()) throw new Error("state folder is not a real folder");
|
|
64
65
|
}
|
|
66
|
+
if (create) ensureStateDirIgnored(directory);
|
|
65
67
|
return directory;
|
|
66
68
|
};
|
|
67
69
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync as nodeSpawnSync } from "node:child_process";
|
|
1
|
+
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
appendFileSync,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "node:fs";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
18
19
|
|
|
19
20
|
export const PACKAGE_NAME = "@litfamily/litclaude";
|
|
20
21
|
const MARKETPLACE_NAME = "litclaude-ai";
|
|
@@ -25,6 +26,9 @@ export const AUTO_UPDATE_TIMEOUT_MS = AUTO_UPDATE_INSTALL_TIMEOUT_MS + AUTO_UPDA
|
|
|
25
26
|
export const AUTO_UPDATE_LOCK_STALE_MS = 60_000;
|
|
26
27
|
export const AUTO_UPDATE_MAX_OUTPUT_BYTES = 128 * 1_024;
|
|
27
28
|
export const AUTO_UPDATE_SCHEMA = 1;
|
|
29
|
+
const REGISTRY_REQUEST_TIMEOUT_MS = 3_000;
|
|
30
|
+
const REGISTRY_MAX_RESPONSE_BYTES = 64 * 1_024;
|
|
31
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
28
32
|
|
|
29
33
|
const stableSemverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
30
34
|
const generationPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
@@ -456,6 +460,131 @@ const resultWithoutSecrets = ({ receiptPath, journalPath, backupPath, ...result
|
|
|
456
460
|
backupPath: backupPath ?? null,
|
|
457
461
|
});
|
|
458
462
|
|
|
463
|
+
const readCacheRecord = (cachePath, now) => {
|
|
464
|
+
try {
|
|
465
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
466
|
+
return validCacheRecord(parsed, now) ? parsed : null;
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// The install child sees only a sanitized `npm_config_registry`, so the version check
|
|
473
|
+
// reads that same registry, and the npm default otherwise.
|
|
474
|
+
const registryLatestUrl = (env = process.env) => {
|
|
475
|
+
const registry = safeUrl(env?.npm_config_registry ?? env?.NPM_CONFIG_REGISTRY) ?? DEFAULT_REGISTRY;
|
|
476
|
+
return new URL(`${encodeURIComponent(PACKAGE_NAME)}/latest`, registry).toString();
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const readCappedBody = async (response) => {
|
|
480
|
+
const length = response.headers.get("content-length");
|
|
481
|
+
if (length !== null && (!/^(0|[1-9]\d*)$/u.test(length) || Number(length) > REGISTRY_MAX_RESPONSE_BYTES)) {
|
|
482
|
+
throw new Error("registry response is too large");
|
|
483
|
+
}
|
|
484
|
+
const chunks = [];
|
|
485
|
+
let size = 0;
|
|
486
|
+
for await (const chunk of response.body ?? []) {
|
|
487
|
+
size += chunk.byteLength;
|
|
488
|
+
if (size > REGISTRY_MAX_RESPONSE_BYTES) throw new Error("registry response is too large");
|
|
489
|
+
chunks.push(Buffer.from(chunk));
|
|
490
|
+
}
|
|
491
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Runs in the detached child. It writes only when the attempt it was started for still
|
|
495
|
+
// owns the cache, so a newer attempt or the terminal notifier is never overwritten.
|
|
496
|
+
export const refreshSessionUpdateCache = async ({
|
|
497
|
+
cachePath,
|
|
498
|
+
generation,
|
|
499
|
+
registryUrl,
|
|
500
|
+
fetch = globalThis.fetch,
|
|
501
|
+
now = () => Date.now(),
|
|
502
|
+
} = {}) => {
|
|
503
|
+
try {
|
|
504
|
+
if (!safeUrl(registryUrl, { allowPath: true })) return false;
|
|
505
|
+
const response = await fetch(registryUrl, {
|
|
506
|
+
headers: { accept: "application/json", "user-agent": "litclaude-ai-update-notifier" },
|
|
507
|
+
redirect: "error",
|
|
508
|
+
signal: AbortSignal.timeout(REGISTRY_REQUEST_TIMEOUT_MS),
|
|
509
|
+
});
|
|
510
|
+
if (response.status !== 200) return false;
|
|
511
|
+
if (!/^application\/json(?:\s*;\s*charset=(?:utf-8|utf8))?$/iu.test(response.headers.get("content-type")?.trim() ?? "")) return false;
|
|
512
|
+
const parsed = JSON.parse(await readCappedBody(response));
|
|
513
|
+
if (parsed?.name !== PACKAGE_NAME) return false;
|
|
514
|
+
parseStableSemver(parsed.version);
|
|
515
|
+
const completedAt = canonicalNow(now);
|
|
516
|
+
const previous = readCacheRecord(cachePath, completedAt);
|
|
517
|
+
if (previous?.generation !== generation) return false;
|
|
518
|
+
writeAtomic(cachePath, `${JSON.stringify({
|
|
519
|
+
schema: 3,
|
|
520
|
+
packageName: PACKAGE_NAME,
|
|
521
|
+
latestVersion: parsed.version,
|
|
522
|
+
checkedAt: new Date(completedAt).toISOString(),
|
|
523
|
+
attemptedAt: previous.attemptedAt,
|
|
524
|
+
generation,
|
|
525
|
+
})}\n`);
|
|
526
|
+
return true;
|
|
527
|
+
} catch {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const spawnSessionCacheRefresh = (cachePath, generation, registryUrl, env = process.env, spawnProcess = nodeSpawn) => {
|
|
533
|
+
const child = spawnProcess(
|
|
534
|
+
process.execPath,
|
|
535
|
+
[fileURLToPath(new URL("./automatic-update-refresh.mjs", import.meta.url)), cachePath, generation, registryUrl],
|
|
536
|
+
{ detached: true, stdio: "ignore", windowsHide: true, env: sanitizeNpmEnvironment(env) },
|
|
537
|
+
);
|
|
538
|
+
child.once("error", () => {});
|
|
539
|
+
child.unref();
|
|
540
|
+
return true;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// SessionStart never waits on the network. It records the attempt (one per 24 hours),
|
|
544
|
+
// hands the registry request to a detached child, and a later session reads the result.
|
|
545
|
+
// A cache file it cannot read as its own record, such as another package's, is left as is.
|
|
546
|
+
const scheduleSessionCacheRefresh = ({ cachePath, env, now, spawnRefresh }) => {
|
|
547
|
+
try {
|
|
548
|
+
const previous = readCacheRecord(cachePath, now);
|
|
549
|
+
if (!previous && existsSync(cachePath)) return "preserved";
|
|
550
|
+
if (previous && now - Date.parse(previous.attemptedAt) < CACHE_TTL_MS) return "throttled";
|
|
551
|
+
const generation = randomUUID();
|
|
552
|
+
writeAtomic(cachePath, `${JSON.stringify({
|
|
553
|
+
schema: 3,
|
|
554
|
+
packageName: PACKAGE_NAME,
|
|
555
|
+
latestVersion: previous?.latestVersion ?? null,
|
|
556
|
+
checkedAt: previous?.checkedAt ?? null,
|
|
557
|
+
attemptedAt: new Date(now).toISOString(),
|
|
558
|
+
generation,
|
|
559
|
+
})}\n`);
|
|
560
|
+
return spawnRefresh(cachePath, generation, registryLatestUrl(env), env) === false ? "spawn-error" : "scheduled";
|
|
561
|
+
} catch {
|
|
562
|
+
return "spawn-error";
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const installedPluginVersion = (claudeHome) => {
|
|
567
|
+
try {
|
|
568
|
+
const registry = JSON.parse(readFileSync(join(claudeHome, "plugins", "installed_plugins.json"), "utf8"));
|
|
569
|
+
const version = registry?.plugins?.["litclaude@litclaude-ai"]?.[0]?.version;
|
|
570
|
+
parseStableSemver(version);
|
|
571
|
+
return version;
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const recentlyFailed = (autoRoot, targetVersion, now) => {
|
|
578
|
+
try {
|
|
579
|
+
const receipt = JSON.parse(readFileSync(join(autoRoot, "receipt.json"), "utf8"));
|
|
580
|
+
if (!["rolled-back", "failed"].includes(receipt?.status) || receipt.targetVersion !== targetVersion) return false;
|
|
581
|
+
const age = now - Date.parse(receipt.finishedAt);
|
|
582
|
+
return age >= 0 && age < CACHE_TTL_MS;
|
|
583
|
+
} catch {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
459
588
|
export const runAutomaticUpdate = ({
|
|
460
589
|
surface = "management",
|
|
461
590
|
command = "install",
|
|
@@ -477,6 +606,7 @@ export const runAutomaticUpdate = ({
|
|
|
477
606
|
spawn = nodeSpawnSync,
|
|
478
607
|
verifyInstall = verifyInstalledVersion,
|
|
479
608
|
restore = restoreTransaction,
|
|
609
|
+
spawnRefresh = spawnSessionCacheRefresh,
|
|
480
610
|
} = {}) => {
|
|
481
611
|
const resolvedLitHome = litHome ?? env.LITCLAUDE_HOME ?? resolveHome();
|
|
482
612
|
const resolvedClaudeHome = claudeHome ?? env.CLAUDE_CONFIG_DIR ?? env.CLAUDE_HOME ?? resolveClaudeHome();
|
|
@@ -493,9 +623,26 @@ export const runAutomaticUpdate = ({
|
|
|
493
623
|
}
|
|
494
624
|
const timestamp = canonicalNow(now);
|
|
495
625
|
const candidate = readCacheCandidate(resolvedCachePath, currentVersion, timestamp);
|
|
626
|
+
if (surface === "session-start" && ["stale-cache", "no-candidate"].includes(candidate.status)) {
|
|
627
|
+
return {
|
|
628
|
+
...candidate,
|
|
629
|
+
refresh: scheduleSessionCacheRefresh({ cachePath: resolvedCachePath, env, now: timestamp, spawnRefresh }),
|
|
630
|
+
};
|
|
631
|
+
}
|
|
496
632
|
if (candidate.status !== "candidate") return candidate;
|
|
633
|
+
// A long-lived Claude Code process keeps running the hook from the version folder it
|
|
634
|
+
// started with, so its own version lags behind what an earlier session installed.
|
|
635
|
+
const installedVersion = installedPluginVersion(resolvedClaudeHome);
|
|
636
|
+
if (installedVersion && compareStableSemver(installedVersion, candidate.version) >= 0) {
|
|
637
|
+
return { status: "already-installed", installedVersion, targetVersion: candidate.version };
|
|
638
|
+
}
|
|
497
639
|
|
|
498
640
|
const autoRoot = automaticUpdateRoot(resolvedLitHome);
|
|
641
|
+
// A failed target stays in the cache until the registry moves on, so without this every
|
|
642
|
+
// new session would spend the whole install budget on it again.
|
|
643
|
+
if (recentlyFailed(autoRoot, candidate.version, timestamp)) {
|
|
644
|
+
return { status: "backoff", targetVersion: candidate.version };
|
|
645
|
+
}
|
|
499
646
|
const lockPath = join(autoRoot, "install.lock");
|
|
500
647
|
let lease;
|
|
501
648
|
try {
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// provider usage fields. This module never returns transcript text, rule bodies,
|
|
5
5
|
// file contents, secrets, or network metadata.
|
|
6
6
|
|
|
7
|
+
import { lstatSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { dirname } from "node:path";
|
|
8
|
-
import { MAX_BOUNDED_FILE_BYTES, readRegularStable } from "./secure-path-read.mjs";
|
|
9
|
+
import { MAX_BOUNDED_FILE_BYTES, readRegularStable, readRegularTail } from "./secure-path-read.mjs";
|
|
9
10
|
import { dedupeByBody } from "./rules/format.mjs";
|
|
10
11
|
|
|
11
12
|
const MAX_TRANSCRIPT_BYTES = MAX_BOUNDED_FILE_BYTES;
|
|
@@ -177,11 +178,71 @@ export const latestUsageTokensFromText = (transcriptText) => {
|
|
|
177
178
|
return latest ?? 0;
|
|
178
179
|
};
|
|
179
180
|
|
|
181
|
+
// A long session's transcript outgrows the whole-file bound, but the latest usage record sits
|
|
182
|
+
// near its end. Read a short tail first and the full bound only when that tail holds none.
|
|
183
|
+
const USAGE_TAIL_WINDOWS = [2 * 1024 * 1024, MAX_TRANSCRIPT_BYTES];
|
|
184
|
+
|
|
185
|
+
// The read starts one byte before the window. When that byte is a newline the window begins on
|
|
186
|
+
// a whole record and keeps it; otherwise everything up to the first newline is a cut fragment.
|
|
187
|
+
export const latestUsageTokensInTail = (transcriptPath, windowBytes) => {
|
|
188
|
+
const result = readRegularTail(dirname(transcriptPath), transcriptPath, windowBytes + 1);
|
|
189
|
+
if (result.failure) return { tokens: 0, failed: true, truncated: false };
|
|
190
|
+
let text = result.bytes.toString("utf8");
|
|
191
|
+
if (result.truncated) {
|
|
192
|
+
const firstBreak = text.indexOf("\n");
|
|
193
|
+
text = firstBreak === -1 ? "" : text.slice(firstBreak + 1);
|
|
194
|
+
}
|
|
195
|
+
return { tokens: latestUsageTokensFromText(text), failed: false, truncated: result.truncated };
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const latestUsageTokensFromTail = (transcriptPath) => {
|
|
199
|
+
for (const window of USAGE_TAIL_WINDOWS) {
|
|
200
|
+
const { tokens, failed, truncated } = latestUsageTokensInTail(transcriptPath, window);
|
|
201
|
+
if (failed) return 0;
|
|
202
|
+
if (tokens > 0 || !truncated) return tokens;
|
|
203
|
+
}
|
|
204
|
+
return 0;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* latestUsageTokens behind a one-entry cache file keyed by the transcript's path, file identity,
|
|
209
|
+
* size and mtime. The status line redraws far more often than the transcript changes, and a
|
|
210
|
+
* long transcript whose last 2 MiB holds no usage record would otherwise cost an 8 MiB read on
|
|
211
|
+
* every redraw. The cache holds only that key and a number; any cache failure just reads.
|
|
212
|
+
*/
|
|
213
|
+
export const latestUsageTokensCached = (transcriptPath, cachePath) => {
|
|
214
|
+
if (typeof transcriptPath !== "string" || transcriptPath.length === 0) return 0;
|
|
215
|
+
let key;
|
|
216
|
+
try {
|
|
217
|
+
const stats = lstatSync(transcriptPath);
|
|
218
|
+
key = { path: transcriptPath, dev: stats.dev, ino: stats.ino, size: stats.size, mtimeMs: stats.mtimeMs };
|
|
219
|
+
} catch {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const cached = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
224
|
+
if (Object.keys(key).every((field) => cached?.[field] === key[field]) && Number.isSafeInteger(cached.tokens)) return cached.tokens;
|
|
225
|
+
} catch {
|
|
226
|
+
// No usable cache entry: measure below.
|
|
227
|
+
}
|
|
228
|
+
const tokens = latestUsageTokens(transcriptPath);
|
|
229
|
+
try {
|
|
230
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
231
|
+
const staging = `${cachePath}.${process.pid}.tmp`;
|
|
232
|
+
writeFileSync(staging, `${JSON.stringify({ ...key, tokens })}\n`);
|
|
233
|
+
renameSync(staging, cachePath);
|
|
234
|
+
} catch {
|
|
235
|
+
// The number is still right; only the next redraw pays for the read again.
|
|
236
|
+
}
|
|
237
|
+
return tokens;
|
|
238
|
+
};
|
|
239
|
+
|
|
180
240
|
/** Match HUD context sizing while applying the same bounded local-file policy. */
|
|
181
241
|
export const latestUsageTokens = (transcriptPath) => {
|
|
182
242
|
if (typeof transcriptPath !== "string" || transcriptPath.length === 0) return 0;
|
|
183
243
|
try {
|
|
184
244
|
const result = readRegularStable(dirname(transcriptPath), transcriptPath, undefined, { maxBytes: MAX_TRANSCRIPT_BYTES });
|
|
245
|
+
if (result.failure === "FILE_TOO_LARGE") return latestUsageTokensFromTail(transcriptPath);
|
|
185
246
|
return result.failure ? 0 : latestUsageTokensFromText(result.bytes.toString("utf8"));
|
|
186
247
|
} catch {
|
|
187
248
|
return 0;
|
|
@@ -14,7 +14,7 @@ export const canonicalSkillResourceManifest = Object.freeze(
|
|
|
14
14
|
["lib/office-runtime-lock/package-lock.json", "4b9552dba69bf2803cecdf71996d26de0497004c00d1e1f5753a5b299f85d8ba"],
|
|
15
15
|
["lib/office-runtime-lock/package.json", "edf7fee2ba78d6ad42bf5761678b2a2210adb7282108864360f83c46adb59708"],
|
|
16
16
|
["lib/office-runtime-lock/requirements.lock", "7c63ce0de1ae9a912d1e546e9c01dd99a705d4a50435f36040d351cc3e6e6014"],
|
|
17
|
-
["lib/office-runtime.mjs", "
|
|
17
|
+
["lib/office-runtime.mjs", "24d108a4ae7aa98dd0d6f2fa66686d7c9f020ecf42fe641241be2869075bbea3"],
|
|
18
18
|
["lib/office_data.py", "0f4c2449622b6d59630e60508594d181279052c41918df4370e11a68df1ee239"],
|
|
19
19
|
["lib/office_runtime_bootstrap.py", "28812ef485f73f413c6d08cfff326290b301a0d7358260a2a0496edd8f71f4e9"],
|
|
20
20
|
["lib/ooxml_integrity.py", "85e5cba28aa3b9f4f4c28f59c6118439a985fe4f29e25274ae63e9ef1edf15e3"],
|
|
@@ -24,20 +24,20 @@ export const canonicalSkillResourceManifest = Object.freeze(
|
|
|
24
24
|
["lib/render_pages.py", "413f5fc234f69493ebbb62dbc3c9d683435fed2ce09c5cf5ae576095a57de071"],
|
|
25
25
|
["lib/rules/constants.mjs", "de97cc08ca834c35ba89a3d384517ae3f24fd52502b8ccd7a3c8227981e3f6c9"],
|
|
26
26
|
["lib/rules/discovery.mjs", "8a3d445c2d55c4d65c721c2724c4f927ffbd6dfa31472be857d9c827274c6456"],
|
|
27
|
-
["lib/rules/engine.mjs", "
|
|
28
|
-
["lib/rules/format.mjs", "
|
|
27
|
+
["lib/rules/engine.mjs", "5c01500bcfd762a7367a0cb9b503c71678e3504eeb2f2873329c0d9321841939"],
|
|
28
|
+
["lib/rules/format.mjs", "c745e01e460f9d57e28881a56af5d1376952b548a33d44feec7914e9fdccbddd"],
|
|
29
29
|
["lib/rules/frontmatter.mjs", "d0272fb934407b20a425a546c7762ba90e6d6d3bbdad083228698e9bd5b58b4b"],
|
|
30
30
|
["lib/rules/glob.mjs", "e0b138279b9608e879af5f680643a0c4aefb00b388e0ba42f98f6eead3c08caf"],
|
|
31
31
|
["lib/rules/matcher.mjs", "83334358a035a9fedf3d7970b2454825d4ca8d0082b5a8779efc8ad8fdeecd5b"],
|
|
32
32
|
["lib/rules/ordering.mjs", "c6da1ef0b228b8f0932b54422d2bc9744eb6bfbb8f5bb70b30b8f1a798765d1c"],
|
|
33
33
|
["lib/rules/scanner.mjs", "c62d672a66af25f29fe1d05b09288583425a2bf55f20d788f81d1578035f79f9"],
|
|
34
|
-
["lib/rules/session-state.mjs", "
|
|
34
|
+
["lib/rules/session-state.mjs", "23d50ebdf80f389b715751d9d0f7096669db0b2e61885d0d2df114d64be18ea0"],
|
|
35
35
|
["lib/secret-shapes.mjs", "7dc58f1caa6d5fc9dc43797a703911e1ec26689a082bb6972f6ed71722a01d57"],
|
|
36
|
-
["lib/secure-path-read.mjs", "
|
|
36
|
+
["lib/secure-path-read.mjs", "31fbfe38a350b5fe3194c84f4cb3a84125dd16a1dc506bae91ff7bd5ff024d56"],
|
|
37
37
|
["lib/strict-json.mjs", "d941f327aabce5ffc212770852cfbc7e08dd03471e4e9bc9560eff54f06d489a"],
|
|
38
38
|
["lib/wikify-knowledge-cli.mjs", "6835f8d6d98a3dd26266409366c23b2da437c0cd41e63537f0f200667655132b"],
|
|
39
|
-
["lib/wikify-knowledge.mjs", "
|
|
40
|
-
["scripts/scaffold-plan.mjs", "
|
|
39
|
+
["lib/wikify-knowledge.mjs", "feaa8d1fc9606b00f5fa49115385a797c285eefdc9e202d94809f6c3cff5d224"],
|
|
40
|
+
["scripts/scaffold-plan.mjs", "6f4b098613f0c22d317aa23cdbe51f4fae64fbfed4cbb0e227b3a4e3e0a97404"],
|
|
41
41
|
["skills/autoconference/SKILL.md", "e034a0232d59bea9f9ed27bb8844a7ee039dd5c7d38aeaffaa32e7ee63daa5ad"],
|
|
42
42
|
["skills/autoresearch/SKILL.md", "2b69c53bade3851cfb9c770bce80afe5f9e75b9629719e9ba95a4b0045eaf5b3"],
|
|
43
43
|
["skills/browser-drive/scripts/capability-probe.mjs", "69fbf97638165d8dac5297c6c2e7b18c5757605acdcaed3ce96f745dc3c72532"],
|
|
@@ -731,7 +731,7 @@ export const canonicalSkillResourceManifest = Object.freeze(
|
|
|
731
731
|
["skills/lit-pptx/templates/tonalities/signal/pack.yaml", "c4c92302e739b1a9f4fde25b2961272207a56a943cb8dde94cbd3a401b05a7d8"],
|
|
732
732
|
["skills/lit-pptx/templates/tonalities/studio/pack.yaml", "ac3164e0333a51282c7c612eccaffbd3144e65e878731304c06f9c5b3cf1da60"],
|
|
733
733
|
["skills/lit-team/SKILL.md", "6d4ca726d1240db7692ccc95802ab6abfd45588147ec663dafa1b3b233a08a13"],
|
|
734
|
-
["skills/lit-team/scripts/team.mjs", "
|
|
734
|
+
["skills/lit-team/scripts/team.mjs", "2c6df317dbe726315676e8632cba0a229653e80a9fe8456eaa617d4185e9577f"],
|
|
735
735
|
["skills/lit-typographic-motion/SKILL.md", "df3fcd7e8c91b9d617cebf0a466ee169f1155d5544984bbfc753f20322491a52"],
|
|
736
736
|
["skills/lit-typographic-motion/engine/NOTICE", "6734178ad953e5f40585d000c13e4803e670d9e8982b7f6ef0395d47a9eedf30"],
|
|
737
737
|
["skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES", "931b4b52cfc52b398124de5f48c51c946d85ff6036643ead116b00f9342aa1b2"],
|
|
@@ -834,7 +834,7 @@ export const canonicalSkillResourceManifest = Object.freeze(
|
|
|
834
834
|
["skills/lit-typographic-motion/references/treatment.md", "382b9454d8de1e9eca1e29b927ecc351972b0fd9853a01b02c6451e8e6e341ed"],
|
|
835
835
|
["skills/lit-typographic-motion/references/type-craft.md", "5bac1461a4ecb64dc4492e7b765934e2ec0d17f848e720f281cb3031b7750903"],
|
|
836
836
|
["skills/lit-typographic-motion/scripts/motion-doctor.mjs", "5c55feeaefbdc042c1c512ff30baabc3568dc7fa0d63dc430cf6b440ec3a4cbb"],
|
|
837
|
-
["skills/lit-typographic-motion/scripts/motion-runtime.mjs", "
|
|
837
|
+
["skills/lit-typographic-motion/scripts/motion-runtime.mjs", "8cf247a25b9ec3c4745f608fd618aeb4e069940cdb97354682066121eea50f47"],
|
|
838
838
|
["skills/lit-typographic-motion/scripts/motion.mjs", "373665f2c82a19f5d703fd74743c95cd6760e7da8ec267797ce964cfa43edf9d"],
|
|
839
839
|
["skills/litresearch/ATTRIBUTION.md", "1ed8b43d48a13d7fc106cbfd86bc1bac0316529bdac4e2e38043fafeae2a8e15"],
|
|
840
840
|
["skills/litwork/SKILL.md", "5c5964d4df69c0e7cc7505f2dec38beed9413e79d64eb61f325012215e86d0ba"],
|