@litfamily/lithermes 1.0.33 → 1.0.34
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/README.md +7 -7
- package/README_Ko-KR.md +7 -7
- package/assets/lithermes-plugin/payload-version.json +11 -11
- package/assets/lithermes-plugin/plugin.yaml +1 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -2
- package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover.mjs +1 -1
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-gate.mjs +1 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-kit.js +42 -3
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +133 -4
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/stage.md +55 -1
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +2 -2
- package/package.json +1 -1
- package/readme-assets/badge-version.svg +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitHermes robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
5
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
4
|
+
<a href="#install"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/badge-version.svg" alt="1.0.34" /></a>
|
|
5
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/badge-license.svg" alt="MIT license" /></a>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
10
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
9
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/lucide-book-open.svg" width="16" alt="" /> Guide</a>
|
|
10
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition film</a>
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
# LitHermes
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
A plugin for **Hermes Agent**. Put `lit` in front of a request, and Hermes plans the task, does it, checks the result and writes down where things stand, so the next session can pick up where this one stopped.
|
|
19
19
|
|
|
20
|
-
**[README and docs on GitHub](https://github.com/wjgoarxiv/lithermes#readme)** · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
20
|
+
**[README and docs on GitHub](https://github.com/wjgoarxiv/lithermes#readme)** · [한국어](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/README_Ko-KR.md)
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
package/README_Ko-KR.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 일곱 로봇 패널이 차례로 켜지고, LitHermes 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
5
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
4
|
+
<a href="#설치"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/badge-version.svg" alt="1.0.34" /></a>
|
|
5
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/badge-license.svg" alt="MIT license" /></a>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
10
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
9
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/docs/guide.ko.md"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/lucide-book-open.svg" width="16" alt="" /> 안내</a>
|
|
10
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/ignition-film.mp4"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/lucide-play.svg" width="16" alt="" /> Ignition 영상</a>
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/lithermes/blob/main/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/readme-assets/lucide-shield-check.svg" width="16" alt="" /> MIT</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
# LitHermes
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
**Hermes Agent**용 플러그인입니다. 요청 앞에 `lit`을 붙이면 Hermes가 작업을 계획하고, 해내고, 결과를 확인한 뒤 어디까지 왔는지 적어 둡니다. 그래서 다음 세션이 그 기록을 읽고 멈춘 자리에서 이어 갈 수 있습니다.
|
|
19
19
|
|
|
20
|
-
**[GitHub의 README와 문서](https://github.com/wjgoarxiv/lithermes/blob/main/README_Ko-KR.md)** · [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.
|
|
20
|
+
**[GitHub의 README와 문서](https://github.com/wjgoarxiv/lithermes/blob/main/README_Ko-KR.md)** · [English](https://cdn.jsdelivr.net/npm/@litfamily/lithermes@1.0.34/README.md)
|
|
21
21
|
|
|
22
22
|
## 설치
|
|
23
23
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-
|
|
2
|
+
"syncedAt": "2026-11-01T12:25:40.311Z",
|
|
3
3
|
"source": "bundled-payload",
|
|
4
|
-
"sourceHash": "
|
|
4
|
+
"sourceHash": "2301b868931c86d3bfeecdd04559f48bea1a0d8edf65879d4c3720755b023e07",
|
|
5
5
|
"files": [
|
|
6
6
|
{
|
|
7
7
|
"path": "NOTICE.md",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
183
|
"path": "plugin.yaml",
|
|
184
|
-
"sha256": "
|
|
184
|
+
"sha256": "d3a858b2ae8b43fe9010b8078777737ab2bb6eae3f32b38b44043df7454e381d"
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
"path": "provider_metrics.py",
|
|
@@ -1329,7 +1329,7 @@
|
|
|
1329
1329
|
},
|
|
1330
1330
|
{
|
|
1331
1331
|
"path": "skills/frontend-ui-ux/references/complete-contract.md",
|
|
1332
|
-
"sha256": "
|
|
1332
|
+
"sha256": "fae8c15d7256482e79a48444972ca1b91ae8ea723d362521648f74b286c12e9a"
|
|
1333
1333
|
},
|
|
1334
1334
|
{
|
|
1335
1335
|
"path": "skills/frontend-ui-ux/references/composition.md",
|
|
@@ -4329,7 +4329,7 @@
|
|
|
4329
4329
|
},
|
|
4330
4330
|
{
|
|
4331
4331
|
"path": "skills/lit-typographic-motion/SKILL.md",
|
|
4332
|
-
"sha256": "
|
|
4332
|
+
"sha256": "a1da7728a3f998c05150a23426be0b7e1a0e000e58ffe5a24289d50a71b8370e"
|
|
4333
4333
|
},
|
|
4334
4334
|
{
|
|
4335
4335
|
"path": "skills/lit-typographic-motion/bin/audio.py",
|
|
@@ -4417,7 +4417,7 @@
|
|
|
4417
4417
|
},
|
|
4418
4418
|
{
|
|
4419
4419
|
"path": "skills/lit-typographic-motion/engine/cover.mjs",
|
|
4420
|
-
"sha256": "
|
|
4420
|
+
"sha256": "0a112972eacbc6b18f7ca1765a26a5044c4b5c76dfd4812338ae7486f831c2d9"
|
|
4421
4421
|
},
|
|
4422
4422
|
{
|
|
4423
4423
|
"path": "skills/lit-typographic-motion/engine/director.mjs",
|
|
@@ -4485,7 +4485,7 @@
|
|
|
4485
4485
|
},
|
|
4486
4486
|
{
|
|
4487
4487
|
"path": "skills/lit-typographic-motion/engine/stage-gate.mjs",
|
|
4488
|
-
"sha256": "
|
|
4488
|
+
"sha256": "7ce876d61ed0cbee9b2e3f847e9da0257113572737b1f7c76649632a2a31ff9d"
|
|
4489
4489
|
},
|
|
4490
4490
|
{
|
|
4491
4491
|
"path": "skills/lit-typographic-motion/engine/stage-init.js",
|
|
@@ -4493,7 +4493,7 @@
|
|
|
4493
4493
|
},
|
|
4494
4494
|
{
|
|
4495
4495
|
"path": "skills/lit-typographic-motion/engine/stage-kit.js",
|
|
4496
|
-
"sha256": "
|
|
4496
|
+
"sha256": "b33f71648fd897c22c119841c1b43226cb11bb5ad855e85040a1dc8d81ca66e2"
|
|
4497
4497
|
},
|
|
4498
4498
|
{
|
|
4499
4499
|
"path": "skills/lit-typographic-motion/engine/stage-qa.mjs",
|
|
@@ -4505,7 +4505,7 @@
|
|
|
4505
4505
|
},
|
|
4506
4506
|
{
|
|
4507
4507
|
"path": "skills/lit-typographic-motion/engine/stage.mjs",
|
|
4508
|
-
"sha256": "
|
|
4508
|
+
"sha256": "03165bde6fdb74ca8ad29e38a835629f672c272cb1e88d43c1c2f64914849a8f"
|
|
4509
4509
|
},
|
|
4510
4510
|
{
|
|
4511
4511
|
"path": "skills/lit-typographic-motion/engine/stills.mjs",
|
|
@@ -4637,7 +4637,7 @@
|
|
|
4637
4637
|
},
|
|
4638
4638
|
{
|
|
4639
4639
|
"path": "skills/lit-typographic-motion/references/stage.md",
|
|
4640
|
-
"sha256": "
|
|
4640
|
+
"sha256": "c02bd6c8373bd684b58ae3f92c178418353465cbefabf310292c17cf8f91490b"
|
|
4641
4641
|
},
|
|
4642
4642
|
{
|
|
4643
4643
|
"path": "skills/lit-typographic-motion/references/studio.md",
|
|
@@ -4941,7 +4941,7 @@
|
|
|
4941
4941
|
},
|
|
4942
4942
|
{
|
|
4943
4943
|
"path": "skills/visual-qa/references/complete-contract.md",
|
|
4944
|
-
"sha256": "
|
|
4944
|
+
"sha256": "393cc99a6fabb2fc40d8d77075833747580e2553614b552012ca1d22acc9e7e5"
|
|
4945
4945
|
},
|
|
4946
4946
|
{
|
|
4947
4947
|
"path": "skills/visual-qa/schemas/evidence-manifest-v1alpha1.schema.json",
|
|
@@ -445,7 +445,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
|
|
|
445
445
|
|
|
446
446
|
```text
|
|
447
447
|
$ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
|
|
448
|
-
Installed LitHermes 1.0.
|
|
448
|
+
Installed LitHermes 1.0.34
|
|
449
449
|
plugin: $ISOLATED/plugins/lithermes
|
|
450
450
|
model config: updated
|
|
451
451
|
|
|
@@ -462,7 +462,7 @@ installed payload: PASS
|
|
|
462
462
|
enabled config: PASS
|
|
463
463
|
|
|
464
464
|
$ hermes lithermes doctor
|
|
465
|
-
[OK] plugin.yaml readable (version 1.0.
|
|
465
|
+
[OK] plugin.yaml readable (version 1.0.34)
|
|
466
466
|
[OK] skills bundled: 40
|
|
467
467
|
[OK] litgoal durable runtime importable
|
|
468
468
|
```
|
|
@@ -64,7 +64,7 @@ Done only when `$M complete --out DIR` exits 0: valid treatment, passing gate (a
|
|
|
64
64
|
|
|
65
65
|
## #contract.hard_stops
|
|
66
66
|
|
|
67
|
-
Exits 10, 11, 12, 14, 15: name the state and its fix (`references/runtime.md`). 16 the treatment field, 17 a stage break, 18 a nondeterministic frame, 19 a network request, 20 a sound fault, 21 no cover encoder, 22 a missing cover asset. Never install during a render, deliver a flash-failing film, or downgrade to pass.
|
|
67
|
+
Exits 10, 11, 12, 14, 15: name the state and its fix (`references/runtime.md`). 16 the treatment field, 17 a stage or clip break, 18 a nondeterministic frame, 19 a network request, 20 a sound fault, 21 no cover encoder, 22 a missing cover asset. Never install during a render, deliver a flash-failing film, or downgrade to pass.
|
|
68
68
|
|
|
69
69
|
## #contract.anti_patterns
|
|
70
70
|
|
|
@@ -102,7 +102,7 @@ function prepare(out, env) {
|
|
|
102
102
|
recordFirstTreatment(out, plan.treatment);
|
|
103
103
|
const coverDir = join(out, COVER_DIR);
|
|
104
104
|
if (!existsSync(join(coverDir, 'index.html'))) throw fail(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: no ${COVER_DIR}/index.html in ${out}; write the cover page first (references/cover.md)`);
|
|
105
|
-
const scan = scanStage(coverDir);
|
|
105
|
+
const scan = scanStage(coverDir, { clips: false });
|
|
106
106
|
if (scan.contract.length) throw fail(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: ${scan.contract.join('; ')}`);
|
|
107
107
|
if (scan.network.length) throw fail(EXIT.STAGE_NETWORK_REQUEST, `STAGE_NETWORK_REQUEST: ${scan.network.join('; ')}; a cover is served offline`);
|
|
108
108
|
const assets = checkAssets({ out, recipe: plan.treatment.recipe, rasters: scan.rasters.paths, coverDir: COVER_DIR });
|
|
@@ -85,6 +85,7 @@ export function renderStageReport(manifest, gate, { out, previewName }) {
|
|
|
85
85
|
lines.push(`path: stage; format ${manifest.format} (${manifest.resolution.join('x')}); ${manifest.durationSec.toFixed(2)} s at ${manifest.fps} fps (${manifest.totalFrames} frames); treatment ${manifest.targetDurationSec} s`);
|
|
86
86
|
lines.push(`renderer: ${manifest.renderer}; flags: ${manifest.chromeFlags.join(' ')}`);
|
|
87
87
|
lines.push(`frame time: p50 ${manifest.frameTimeMs.p50} ms, p95 ${manifest.frameTimeMs.p95} ms over ${manifest.frameTimeMs.captured} captures; master ${manifest.masterSec?.toFixed(1)} s`);
|
|
88
|
+
if (manifest.scan?.clips?.length) lines.push(`clips: ${manifest.scan.clips.map((c) => `${c.id} ${c.count} frames at ${c.fps} fps`).join(', ')} (keyed overlays, chosen from film time)`);
|
|
88
89
|
if (manifest.sound) lines.push(`sound: ${manifest.sound.label}`);
|
|
89
90
|
lines.push(`QA gate: ${gate.failed.length ? 'FAIL' : 'PASS'}`);
|
|
90
91
|
for (const rule of gate.rules) lines.push(`${rule.id}: ${rule.status} — ${rule.detail}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// LitHermes stage kit, served to stage pages at /lit/stage-kit.js.
|
|
2
2
|
// Motion primitives only: easing, springs, keyframes, timing, seeded
|
|
3
|
-
// randomness, text splitting, path drawing and morphing, masks, colour mixing
|
|
4
|
-
// and
|
|
3
|
+
// randomness, text splitting, path drawing and morphing, masks, colour mixing,
|
|
4
|
+
// text registration and declared keyed clips. It ships no scene, object, layout or copy; the page
|
|
5
5
|
// draws whatever its treatment calls for. Every function is a pure function of
|
|
6
6
|
// its arguments (and of t), so a frame depends only on the virtual clock.
|
|
7
7
|
(() => {
|
|
@@ -374,6 +374,45 @@
|
|
|
374
374
|
return target;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// A declared keyed clip (stage/clips/clips.json, served as window.LitClips by /lit/clips.js). One absolutely
|
|
378
|
+
// placed wrapper at `box` holds the clip's frame for t; the frame index is floor((t - start) * fps) + 1 counted
|
|
379
|
+
// from `from`, a pure function of film time, so no <video> and no real clock is involved. Outside from..to the
|
|
380
|
+
// wrapper is hidden (null returned) unless `hold` keeps the last frame or `loop` wraps by frame index. The page
|
|
381
|
+
// scales or fades the returned wrapper; the <img> inside is replaced on a frame change and the renderer waits
|
|
382
|
+
// for its decode before it captures.
|
|
383
|
+
function clip({ parent, id, box } = {}) {
|
|
384
|
+
const all = window.LitClips;
|
|
385
|
+
if (!all) throw new Error('LitStage.clip: load <script src="/lit/clips.js"></script> after the kit; the stage declares no clips (clips/clips.json)');
|
|
386
|
+
const c = all[id];
|
|
387
|
+
if (!c) throw new Error(`LitStage.clip: no clip "${id}" in clips/clips.json (declared: ${Object.keys(all).join(', ')})`);
|
|
388
|
+
if (!parent || !box || !(box.w > 0) || !(box.h > 0)) throw new Error('LitStage.clip: needs { parent, id, box: { x, y, w, h } }');
|
|
389
|
+
const wrap = document.createElement('div');
|
|
390
|
+
wrap.setAttribute('data-lit-clip', id);
|
|
391
|
+
Object.assign(wrap.style, { position: 'absolute', left: `${box.x}px`, top: `${box.y}px`, width: `${box.w}px`, height: `${box.h}px`, display: 'none', pointerEvents: 'none' });
|
|
392
|
+
parent.append(wrap);
|
|
393
|
+
let shown = 0;
|
|
394
|
+
let img = null;
|
|
395
|
+
return (t, { start = 0, from = 1, to = c.count, loop = false, hold = false } = {}) => {
|
|
396
|
+
if (!(from >= 1 && to <= c.count && from <= to)) throw new Error(`LitStage.clip: from..to must lie within 1..${c.count} (got ${from}..${to})`);
|
|
397
|
+
const n = to - from + 1;
|
|
398
|
+
const k = Math.floor((t - start) * c.fps + 1e-6);
|
|
399
|
+
const i = k < 0 ? 0 : k < n ? from + k : loop ? from + (k % n) : hold ? to : 0;
|
|
400
|
+
wrap.style.display = i ? 'block' : 'none';
|
|
401
|
+
if (!i) return null;
|
|
402
|
+
if (i !== shown) {
|
|
403
|
+
const next = document.createElement('img');
|
|
404
|
+
next.src = `${c.src}/frame_${String(i).padStart(4, '0')}.png`;
|
|
405
|
+
next.alt = '';
|
|
406
|
+
Object.assign(next.style, { display: 'block', width: '100%', height: '100%', objectFit: 'contain' });
|
|
407
|
+
if (img) img.replaceWith(next);
|
|
408
|
+
else wrap.append(next);
|
|
409
|
+
img = next;
|
|
410
|
+
shown = i;
|
|
411
|
+
}
|
|
412
|
+
return wrap;
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
377
416
|
function define(film) {
|
|
378
417
|
if (!film || typeof film !== 'object') throw new Error('LitStage.define needs { width, height, fps, duration, render }');
|
|
379
418
|
for (const key of ['width', 'height', 'duration']) if (!Number.isFinite(film[key])) throw new Error(`LitStage.define: ${key} must be a number`);
|
|
@@ -384,6 +423,6 @@
|
|
|
384
423
|
|
|
385
424
|
window.LitStage = Object.freeze({
|
|
386
425
|
ease, bezier, spring, kf, at, stagger, seq, rand, splitText, drawPath, morph,
|
|
387
|
-
clipInset, clipCircle, maskWipe, mix, text, define,
|
|
426
|
+
clipInset, clipCircle, maskWipe, mix, text, define, clip,
|
|
388
427
|
});
|
|
389
428
|
})();
|
|
@@ -22,6 +22,7 @@ import { runTextQa } from './stage-qa.mjs';
|
|
|
22
22
|
import { readStyle, styleCss, styleScript } from './styles.mjs';
|
|
23
23
|
import { addToStills, writeBoard } from './board.mjs';
|
|
24
24
|
import { FONT_FILES } from './frame.mjs';
|
|
25
|
+
import { decodePng } from './cover-png.mjs';
|
|
25
26
|
|
|
26
27
|
import { findCache, fontFile, fontState, hangulPath, runtimeRequire, WARM } from '../bin/runtime.mjs';
|
|
27
28
|
|
|
@@ -54,6 +55,12 @@ export const CONTENT_TYPES = Object.freeze({
|
|
|
54
55
|
const RASTER = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
55
56
|
const TEXTUAL = new Set(['.html', '.js', '.mjs', '.css', '.svg', '.json']);
|
|
56
57
|
export const RASTER_LIMITS = Object.freeze({ count: 24, bytes: 8 * 1024 * 1024, sameSize: 10 });
|
|
58
|
+
// Declared keyed clips (references/stage.md, Clips) answer to these caps instead of RASTER_LIMITS. A clip
|
|
59
|
+
// is a sticker on the film, never the film: 8-bit RGBA PNG frames, at most `side` px on the long side.
|
|
60
|
+
export const CLIP_LIMITS = Object.freeze({
|
|
61
|
+
clips: 12, framesPerClip: 360, framesTotal: 1500, bytesPerClip: 40 * 1024 * 1024, bytesTotal: 128 * 1024 * 1024, side: 720, fps: 30,
|
|
62
|
+
});
|
|
63
|
+
export const CLIPS_DIR = 'clips';
|
|
57
64
|
|
|
58
65
|
// The software rung only (SwiftShader, CPU raster): a mismatch between two
|
|
59
66
|
// captures can then only come from the page. Plus the stage set and the two
|
|
@@ -171,14 +178,130 @@ function walk(dir, root, out = []) {
|
|
|
171
178
|
return out;
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Declared clips: stage/clips/clips.json and the frame folders it names.
|
|
183
|
+
|
|
184
|
+
const CLIP_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/u;
|
|
185
|
+
const CLIP_DIR = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/u;
|
|
186
|
+
const clipFrame = (i) => `frame_${String(i).padStart(4, '0')}.png`;
|
|
187
|
+
const mb = (bytes) => (bytes / 1048576).toFixed(1);
|
|
188
|
+
|
|
189
|
+
// The clips clips.json declares ([] without the file), checked for shape. Throws a plain Error naming the break.
|
|
190
|
+
function declaredClips(root, limits = CLIP_LIMITS) {
|
|
191
|
+
const file = join(root, CLIPS_DIR, 'clips.json');
|
|
192
|
+
if (!existsSync(file)) return [];
|
|
193
|
+
let doc;
|
|
194
|
+
try { doc = JSON.parse(readFileSync(file, 'utf8')); } catch (error) { throw new Error(`${CLIPS_DIR}/clips.json is not valid JSON (${error.message})`); }
|
|
195
|
+
if (!doc || !Array.isArray(doc.clips)) throw new Error(`${CLIPS_DIR}/clips.json needs { "clips": [ { "id", "dir", "count", "fps" } ] }`);
|
|
196
|
+
if (doc.clips.length > limits.clips) throw new Error(`${doc.clips.length} clips declared (at most ${limits.clips} clips)`);
|
|
197
|
+
const ids = new Set(), dirs = new Set();
|
|
198
|
+
return doc.clips.map((c, n) => {
|
|
199
|
+
const at = `${CLIPS_DIR}/clips.json clip #${n + 1}`;
|
|
200
|
+
if (!c || typeof c !== 'object') throw new Error(`${at} is not an object`);
|
|
201
|
+
if (typeof c.id !== 'string' || !CLIP_ID.test(c.id)) throw new Error(`${at}: id must be a plain token of letters, digits, - and _ (got ${JSON.stringify(c.id)})`);
|
|
202
|
+
if (ids.has(c.id)) throw new Error(`${at}: duplicate id ${c.id}`);
|
|
203
|
+
ids.add(c.id);
|
|
204
|
+
if (typeof c.dir !== 'string' || !CLIP_DIR.test(c.dir) || c.dir.includes('..')) throw new Error(`clip "${c.id}": dir must be a folder name directly under ${CLIPS_DIR}/ (got ${JSON.stringify(c.dir)})`);
|
|
205
|
+
if (dirs.has(c.dir)) throw new Error(`clip "${c.id}": dir ${c.dir} is already declared by another clip`);
|
|
206
|
+
dirs.add(c.dir);
|
|
207
|
+
if (!Number.isInteger(c.count) || c.count < 1) throw new Error(`clip "${c.id}": count must be a whole number of frames (got ${JSON.stringify(c.count)})`);
|
|
208
|
+
if (!Number.isInteger(c.fps) || c.fps < 1 || c.fps > limits.fps) throw new Error(`clip "${c.id}": fps must be a whole number from 1 to ${limits.fps} (got ${JSON.stringify(c.fps)})`);
|
|
209
|
+
return { id: c.id, dir: c.dir, count: c.count, fps: c.fps };
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// One declared clip against its bytes: exactly frame_0001.png..frame_<count>.png in clips/<dir>/, each an 8-bit
|
|
214
|
+
// non-animated RGBA PNG of one size within the overlay cap, real transparency in the first, middle and last frame.
|
|
215
|
+
function checkClip(d, byRel, limits) {
|
|
216
|
+
const fail = (message) => new Error(`clip "${d.id}": ${message}`);
|
|
217
|
+
if (d.count > limits.framesPerClip) throw fail(`${d.count} frames (at most ${limits.framesPerClip} per clip)`);
|
|
218
|
+
const names = [];
|
|
219
|
+
for (let i = 1; i <= d.count; i++) {
|
|
220
|
+
const rel = join(CLIPS_DIR, d.dir, clipFrame(i));
|
|
221
|
+
if (!byRel.has(rel)) throw fail(`${CLIPS_DIR}/${d.dir}/${clipFrame(i)} is missing (declared ${d.count} frames, named frame_0001.png onward)`);
|
|
222
|
+
names.push(rel);
|
|
223
|
+
}
|
|
224
|
+
const prefix = join(CLIPS_DIR, d.dir) + sep;
|
|
225
|
+
const stray = [...byRel.keys()].find((rel) => rel.startsWith(prefix) && !names.includes(rel));
|
|
226
|
+
if (stray) throw fail(`${stray.split(sep).join('/')} is not one of the ${d.count} declared frames; raise count or remove it`);
|
|
227
|
+
const bytes = names.reduce((sum, rel) => sum + statSync(byRel.get(rel)).size, 0);
|
|
228
|
+
if (bytes > limits.bytesPerClip) throw fail(`${mb(bytes)} MB (at most ${(limits.bytesPerClip / 1048576).toFixed(0)} MB per clip)`);
|
|
229
|
+
let size = null;
|
|
230
|
+
for (const rel of names) {
|
|
231
|
+
const data = readFileSync(byRel.get(rel));
|
|
232
|
+
const shown = rel.split(sep).join('/');
|
|
233
|
+
if (data.length < 33 || data.readUInt32BE(12) !== 0x49484452 || data.toString('latin1', 1, 4) !== 'PNG') throw fail(`${shown} is not a readable PNG`);
|
|
234
|
+
if (isAnimatedRaster(data, '.png')) throw fail(`${shown} is an animated image; a clip is a folder of still frames`);
|
|
235
|
+
if (data[25] !== 6 || data[24] !== 8) throw fail(`${shown} must be an 8-bit RGBA PNG with an alpha channel (a keyed frame), not an opaque image`);
|
|
236
|
+
const [width, height] = imageSize(data, '.png');
|
|
237
|
+
if (Math.max(width, height) > limits.side) throw fail(`${shown} is ${width}x${height}; a clip is an overlay of at most ${limits.side} px on the long side`);
|
|
238
|
+
if (size && (size[0] !== width || size[1] !== height)) throw fail(`${shown} is ${width}x${height} but frame_0001.png is ${size[0]}x${size[1]}; one size per clip`);
|
|
239
|
+
size = [width, height];
|
|
240
|
+
}
|
|
241
|
+
for (const rel of [names[0], names[Math.floor((names.length - 1) / 2)], names[names.length - 1]]) {
|
|
242
|
+
let rgba;
|
|
243
|
+
try { ({ rgba } = decodePng(readFileSync(byRel.get(rel)))); } catch (error) { throw fail(`${rel.split(sep).join('/')} cannot be decoded (${error.message}); use a non-interlaced 8-bit RGBA PNG`); }
|
|
244
|
+
let transparent = false;
|
|
245
|
+
for (let i = 3; i < rgba.length && !transparent; i += 4) transparent = rgba[i] < 255;
|
|
246
|
+
if (!transparent) throw fail(`${rel.split(sep).join('/')} has no transparent pixel; a clip is keyed, not a full-frame video`);
|
|
247
|
+
}
|
|
248
|
+
return { ...d, width: size[0], height: size[1], bytes, frames: names };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check every declared clip. Never throws: returns { clips, problems, claimed }, where `clips` are the clips that
|
|
253
|
+
* passed ({ id, dir, count, fps, width, height, bytes, frames }), `problems` are the contract findings, and
|
|
254
|
+
* `claimed` is the set of stage-relative raster paths inside declared clip folders, which are not page rasters.
|
|
255
|
+
*/
|
|
256
|
+
export function scanClips(stageDir, { limits = CLIP_LIMITS, files = null } = {}) {
|
|
257
|
+
const root = realpathSync(stageDir);
|
|
258
|
+
const problems = [];
|
|
259
|
+
let decls;
|
|
260
|
+
try { decls = declaredClips(root, limits); } catch (error) { return { clips: [], problems: [error.message], claimed: new Set() }; }
|
|
261
|
+
const byRel = new Map();
|
|
262
|
+
for (const f of files || walk(root, root)) if (!f.escape && RASTER.has(extname(f.path).toLowerCase())) byRel.set(relative(root, f.path), f.real);
|
|
263
|
+
const claimed = new Set();
|
|
264
|
+
const clips = [];
|
|
265
|
+
let frames = 0, bytes = 0;
|
|
266
|
+
for (const d of decls) {
|
|
267
|
+
const prefix = join(CLIPS_DIR, d.dir) + sep;
|
|
268
|
+
const mine = new Map([...byRel].filter(([rel]) => rel.startsWith(prefix)));
|
|
269
|
+
for (const rel of mine.keys()) claimed.add(rel);
|
|
270
|
+
try { clips.push(checkClip(d, mine, limits)); } catch (error) { problems.push(error.message); }
|
|
271
|
+
}
|
|
272
|
+
for (const c of clips) { frames += c.count; bytes += c.bytes; }
|
|
273
|
+
if (frames > limits.framesTotal) problems.push(`${frames} clip frames in all (at most ${limits.framesTotal})`);
|
|
274
|
+
if (bytes > limits.bytesTotal) problems.push(`${mb(bytes)} MB of clip frames in all (at most ${(limits.bytesTotal / 1048576).toFixed(0)} MB)`);
|
|
275
|
+
return { clips, problems, claimed };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* window.LitClips for LitStage.clip: id -> { src, count, fps, width, height }, from clips.json and the first frame's
|
|
280
|
+
* header. null when the stage declares no clips, or declares ones the scan would refuse.
|
|
281
|
+
*/
|
|
282
|
+
export function clipsScript(stageDir) {
|
|
283
|
+
try {
|
|
284
|
+
const root = realpathSync(stageDir);
|
|
285
|
+
const table = Object.fromEntries(declaredClips(root).map((c) => {
|
|
286
|
+
const [width, height] = imageSize(readFileSync(join(root, CLIPS_DIR, c.dir, clipFrame(1))), '.png');
|
|
287
|
+
return [c.id, { src: `${CLIPS_DIR}/${c.dir}`, count: c.count, fps: c.fps, width, height }];
|
|
288
|
+
}));
|
|
289
|
+
return Object.keys(table).length ? `window.LitClips = ${JSON.stringify(table)};\n` : null;
|
|
290
|
+
} catch { return null; }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// `clips: false` (cover mode) ignores clips.json, so a clip folder is held to the raster limits.
|
|
294
|
+
export function scanStage(stageDir, { clips: allowClips = true } = {}) {
|
|
175
295
|
const root = realpathSync(stageDir);
|
|
176
296
|
const contract = [], network = [];
|
|
177
297
|
const files = walk(root, root);
|
|
298
|
+
const clipScan = allowClips ? scanClips(root, { files }) : { clips: [], problems: [], claimed: new Set() };
|
|
299
|
+
contract.push(...clipScan.problems);
|
|
178
300
|
const rasters = [];
|
|
179
301
|
for (const file of files) {
|
|
180
302
|
const rel = relative(root, file.path);
|
|
181
303
|
if (file.escape) { contract.push(`${rel} is a symlink that leaves the stage dir`); continue; }
|
|
304
|
+
if (clipScan.claimed.has(rel)) continue;
|
|
182
305
|
const ext = extname(file.path).toLowerCase();
|
|
183
306
|
const bytes = readFileSync(file.real);
|
|
184
307
|
if (RASTER.has(ext)) {
|
|
@@ -199,7 +322,8 @@ export function scanStage(stageDir) {
|
|
|
199
322
|
const bySize = new Map();
|
|
200
323
|
for (const r of rasters) if (r.size) { const key = r.size.join('x'); bySize.set(key, (bySize.get(key) || 0) + 1); }
|
|
201
324
|
for (const [key, count] of bySize) if (count >= RASTER_LIMITS.sameSize) contract.push(`${count} rasters of the same size ${key} read as a flipbook; draw motion, do not play frames`);
|
|
202
|
-
|
|
325
|
+
const clips = clipScan.clips.map(({ id, count, fps, width, height, bytes }) => ({ id, count, fps, width, height, bytes }));
|
|
326
|
+
return { contract: [...new Set(contract)], network: [...new Set(network)], files: files.length, rasters: { count: rasters.length, bytes: total, paths: rasters.map((r) => r.rel) }, clips };
|
|
203
327
|
}
|
|
204
328
|
|
|
205
329
|
// ---------------------------------------------------------------------------
|
|
@@ -312,6 +436,7 @@ export async function openStage({ runtime, stageDir, profileRoot, width, height,
|
|
|
312
436
|
const cdp = await context.newCDPSession(page);
|
|
313
437
|
const root = realpathSync(stageDir);
|
|
314
438
|
const kit = readFileSync(join(here, 'stage-kit.js'));
|
|
439
|
+
const clipsJs = clipsScript(root);
|
|
315
440
|
const blocked = [], contract = [];
|
|
316
441
|
const serve = async (event) => {
|
|
317
442
|
const { requestId, request } = event;
|
|
@@ -329,6 +454,7 @@ export async function openStage({ runtime, stageDir, profileRoot, width, height,
|
|
|
329
454
|
const path = decodeURIComponent(url.pathname);
|
|
330
455
|
if (path === '/lit/stage-kit.js') return reply(200, CONTENT_TYPES['.js'], kit);
|
|
331
456
|
if (path === '/lit/fonts.css') return reply(200, CONTENT_TYPES['.css'], fontCss);
|
|
457
|
+
if (path === '/lit/clips.js' && clipsJs) return reply(200, CONTENT_TYPES['.js'], clipsJs);
|
|
332
458
|
if (path === '/lit/style.css' && style) return reply(200, CONTENT_TYPES['.css'], style.css);
|
|
333
459
|
if (path === '/lit/style.js' && style) return reply(200, CONTENT_TYPES['.js'], style.js);
|
|
334
460
|
if (path.startsWith('/lit/three/')) {
|
|
@@ -448,9 +574,12 @@ function beatFramesOf(treatment, fps, totalFrames) {
|
|
|
448
574
|
});
|
|
449
575
|
}
|
|
450
576
|
|
|
451
|
-
|
|
577
|
+
// The rasters decoded before frame 0. Declared clip frames are left out: a page decodes each one when it shows it.
|
|
578
|
+
export function imageUrlsIn(stageDir) {
|
|
452
579
|
const root = realpathSync(stageDir);
|
|
453
|
-
|
|
580
|
+
let prefixes = [];
|
|
581
|
+
try { prefixes = declaredClips(root).map((c) => join(CLIPS_DIR, c.dir) + sep); } catch { /* the scan already refused a bad declaration */ }
|
|
582
|
+
return walk(root, root).filter((f) => !f.escape && RASTER.has(extname(f.path).toLowerCase()) && !prefixes.some((p) => relative(root, f.path).startsWith(p)))
|
|
454
583
|
.map((f) => `${ORIGIN}/${relative(root, f.path).split(sep).map(encodeURIComponent).join('/')}`);
|
|
455
584
|
}
|
|
456
585
|
|
|
@@ -17,7 +17,8 @@ deliverable.
|
|
|
17
17
|
Never install, download or copy a third-party library into the stage dir. Use
|
|
18
18
|
the kit below and your own code. Rasters are textures and stills, never a frame
|
|
19
19
|
sequence: at most 24 raster images and 8 MB in total; 10 or more rasters of the
|
|
20
|
-
same size (a flipbook), or any animated GIF, APNG or WebP, is exit 17.
|
|
20
|
+
same size (a flipbook), or any animated GIF, APNG or WebP, is exit 17. The one
|
|
21
|
+
frame sequence a page may carry is a declared keyed clip (Clips).
|
|
21
22
|
|
|
22
23
|
Put all on-screen copy in one `COPY` object at the top of `stage/index.html`,
|
|
23
24
|
so the copy is easy to find and edit:
|
|
@@ -43,6 +44,8 @@ declares the film:
|
|
|
43
44
|
1080×1920 for 9:16. `duration` is the treatment's `durationSec`; the render
|
|
44
45
|
must land within ±10 % of it.
|
|
45
46
|
- `fps` is 60. Use 30 only when the treatment says so.
|
|
47
|
+
- A page that shows clips also loads `<script src="/lit/clips.js"></script>`
|
|
48
|
+
after the kit (Clips).
|
|
46
49
|
- `render(t)` draws the frame at time `t` in seconds. It may be omitted when
|
|
47
50
|
the page animates only with CSS or the Web Animations API.
|
|
48
51
|
- Setting `window.litStage` to the same object is equivalent to calling
|
|
@@ -168,6 +171,57 @@ scenes, no objects, no layouts, no copy. `window.LitStage` holds:
|
|
|
168
171
|
- **Text registration.** `LitStage.text(el, { decor: true })` and
|
|
169
172
|
`LitStage.text({ content, x, y, w, h })`, above.
|
|
170
173
|
|
|
174
|
+
- **Clips.** `LitStage.clip({ parent, id, box })` registers a declared keyed
|
|
175
|
+
clip once and returns `show(t, opts)`, which `render(t)` calls every frame
|
|
176
|
+
(Clips).
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
const boom = LitStage.clip({ parent: layer, id: "boom", box: { x: 1200, y: 300, w: 520, h: 511 } });
|
|
180
|
+
boom(t, { start: 2.1 });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Clips
|
|
184
|
+
|
|
185
|
+
Real keyed footage (a reaction clip, a spinning loop shot on a green screen) joins the
|
|
186
|
+
film as a layer of the page. The footage is keyed outside the renderer into one 8-bit RGBA
|
|
187
|
+
PNG per frame, the frames are declared, and the page picks the frame from film time. Nothing
|
|
188
|
+
is encoded by hand: the renderer captures the page, so the flash audit, the determinism
|
|
189
|
+
replay and the text checks see the final pixels like any other film.
|
|
190
|
+
|
|
191
|
+
1. Put each clip's frames in the run's stage folder as `stage/clips/<dir>/frame_0001.png`,
|
|
192
|
+
`frame_0002.png` and on, with no gap and no other image in that folder.
|
|
193
|
+
2. Declare the clips in `stage/clips/clips.json`:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{ "clips": [ { "id": "boom", "dir": "01-boom", "count": 122, "fps": 30 } ] }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`id` is the name the page uses, `dir` a folder directly under `clips/`, `count` the number
|
|
200
|
+
of frames and `fps` the clip's own rate, 1 to 30. Declare a clip once even if it plays twice.
|
|
201
|
+
3. The page loads `<script src="/lit/clips.js"></script>` after the kit. For each place a clip
|
|
202
|
+
appears it calls `LitStage.clip({ parent, id, box })` once, outside `render`, and calls the
|
|
203
|
+
returned `show(t, opts)` from `render(t)`. `parent` holds the clip, `box` is its place in
|
|
204
|
+
pixels and the frame is fitted inside it. `show` returns the clip's wrapper element, or
|
|
205
|
+
`null` while the clip is hidden; scale, fade or move that wrapper from `t` like any layer.
|
|
206
|
+
|
|
207
|
+
The frame is `floor((t - start) * fps) + 1`, counted from `from`, so a cue is plain
|
|
208
|
+
arithmetic: `start = cue - (N - 1) / fps` puts frame N on the cue. A clip whose own sound
|
|
209
|
+
sits on frame 85 and has to land on a cue at 0.12 s takes `start = 0.12 - 84 / 30`. Options
|
|
210
|
+
are `start` (film seconds, default 0), `from` and `to` (the frame range, default the whole
|
|
211
|
+
clip), `loop` (wrap by frame index) and `hold` (keep the last frame). Before `start`, and after
|
|
212
|
+
`to` without `loop` or `hold`, the clip is hidden.
|
|
213
|
+
|
|
214
|
+
The pre-flight scan refuses with exit 17, naming the clip: a stage holds at most 12 clips; a
|
|
215
|
+
clip holds at most 360 frames and the stage 1500 in all; more than 40 MB in a clip or 128 MB in
|
|
216
|
+
all; a frame whose long side is above 720 px (a clip is an overlay, not the film); a frame rate
|
|
217
|
+
above 30; frames of mixed sizes; a missing or extra frame; a frame that is not an 8-bit RGBA
|
|
218
|
+
PNG, or has no transparent pixel in the first, middle and last frame (an opaque video is not
|
|
219
|
+
keyed); an animated image; a duplicate `id`; and a `dir` that leaves `clips/`. A folder that is
|
|
220
|
+
not declared, and any frame sequence beside a declared clip, is a flipbook as before. Cover mode takes no clips,
|
|
221
|
+
so a declaration there changes nothing and the folder meets the raster limits.
|
|
222
|
+
The text checks read DOM text, so keep clips clear of copy; a clip laid over a line is yours to
|
|
223
|
+
catch on the stills. The manifest and the gate report list each clip.
|
|
224
|
+
|
|
171
225
|
## How the capture works
|
|
172
226
|
|
|
173
227
|
The renderer serves the page to a software-rendered Chrome (SwiftShader, CPU
|
|
@@ -485,7 +485,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
|
|
|
485
485
|
|
|
486
486
|
```text
|
|
487
487
|
$ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
|
|
488
|
-
Installed LitHermes 1.0.
|
|
488
|
+
Installed LitHermes 1.0.34
|
|
489
489
|
plugin: $ISOLATED/plugins/lithermes
|
|
490
490
|
model config: updated
|
|
491
491
|
|
|
@@ -502,7 +502,7 @@ installed payload: PASS
|
|
|
502
502
|
enabled config: PASS
|
|
503
503
|
|
|
504
504
|
$ hermes lithermes doctor
|
|
505
|
-
[OK] plugin.yaml readable (version 1.0.
|
|
505
|
+
[OK] plugin.yaml readable (version 1.0.34)
|
|
506
506
|
[OK] skills bundled: 40
|
|
507
507
|
[OK] litgoal durable runtime importable
|
|
508
508
|
```
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.34"><title>release: 1.0.34</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.34</text></g></svg>
|