herm-tui 1.6.0-dev.12 → 1.6.0-dev.14
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/assets/skills/eikon/SKILL.md +51 -73
- package/assets/skills/eikon-create/SKILL.md +151 -0
- package/db.worker.js +2 -2
- package/index.js +37 -37
- package/package.json +1 -1
|
@@ -1,83 +1,61 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eikon
|
|
3
|
-
description:
|
|
3
|
+
description: Guide the user through making or editing a herm sidebar avatar (eikon) using herm's built-in Eikon Studio tab. The agent's role is advisory (what makes a good source, which knob to reach for); source generation is /eikon-create and all rasterize/bake happens in-app.
|
|
4
|
+
related_skills: [eikon-create]
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
+
# Building an eikon in herm
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
An eikon is a 48×24 monochrome text avatar. It lives on disk as:
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
~/.hermes/eikons/<name>/
|
|
12
|
+
<name>.eikon packed NDJSON — written by Studio on Ctrl+S
|
|
13
|
+
studio.json Studio's workspace state
|
|
14
|
+
source/ base.<ext>, <state>.<ext>
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
You do **not** write `.eikon` or `studio.json`. Studio does.
|
|
13
17
|
|
|
18
|
+
## Where the user does the work
|
|
19
|
+
|
|
20
|
+
Herm's built-in **Eikon** tab (Studio / Gallery). Tell the user: "open
|
|
21
|
+
the Eikon tab" or "Ctrl+K → Eikon". In Studio:
|
|
22
|
+
|
|
23
|
+
- `eikon` row → pick / New…
|
|
24
|
+
- `source` row → Local file… / Generate image… / Generate video…
|
|
25
|
+
- `input` section → contrast / invert / flip (pixel-domain, shared)
|
|
26
|
+
- `<rasterizer>` section → symbols / fill / dither (glyph-domain)
|
|
27
|
+
- Preview pane → wheel pans, Ctrl+wheel zooms, Shift+wheel pans X
|
|
28
|
+
- **Ctrl+S** bakes all six states and sets it active
|
|
29
|
+
|
|
30
|
+
## What makes a good source
|
|
31
|
+
|
|
32
|
+
One line, once: **48×24, one color. Light subject on black, high
|
|
33
|
+
contrast, strong silhouette.** Fine detail disappears; outline is
|
|
34
|
+
everything.
|
|
35
|
+
|
|
36
|
+
## When to do what
|
|
37
|
+
|
|
38
|
+
| user says | you do |
|
|
39
|
+
|---|---|
|
|
40
|
+
| "make me an eikon of X" | Load `eikon-create` and follow it. |
|
|
41
|
+
| drops an image path | `cp` it to `~/.hermes/eikons/<name>/source/base.<ext>` → "Eikon tab, pick <name>". |
|
|
42
|
+
| "edit my <name> eikon" | "Eikon tab → `eikon` row → <name>." |
|
|
43
|
+
| "too dark / washed out" | "invert toggle, then contrast slider — under `input`." |
|
|
44
|
+
| "off-center / too small" | "Ctrl+wheel to zoom, wheel/drag to pan on the preview." |
|
|
45
|
+
| "make it move" | `eikon-create` §5 (video), or Studio's `source` → Generate video…. |
|
|
46
|
+
|
|
47
|
+
## Quick poster
|
|
48
|
+
|
|
49
|
+
To show a candidate in chat without Studio:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
chafa --size=48x24 --symbols=braille --colors=none --format=symbols --stretch "<path>"
|
|
14
53
|
```
|
|
15
|
-
~/.hermes/eikons/<name>/
|
|
16
|
-
<name>.eikon packed NDJSON (48×24, 6 states)
|
|
17
|
-
studio.json { rasterizer, spatial:{zoom,ox,oy}, base:{}, per:{}, glyph, sources:{} }
|
|
18
|
-
source/ base.png (or .jpg/.webp); optionally idle.png, error.png …
|
|
19
|
-
```
|
|
20
54
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
3. **Adopt + seed.** One shell block:
|
|
30
|
-
```bash
|
|
31
|
-
NAME=…; SRC=…
|
|
32
|
-
mkdir -p ~/.hermes/eikons/$NAME/source
|
|
33
|
-
cp "$SRC" ~/.hermes/eikons/$NAME/source/base.${SRC##*.}
|
|
34
|
-
cat > ~/.hermes/eikons/$NAME/studio.json <<EOF
|
|
35
|
-
{"rasterizer":"chafa","spatial":{"zoom":1,"ox":0.5,"oy":0.5},"base":{},"per":{},"glyph":"◆","sources":{"base":"base.${SRC##*.}"}}
|
|
36
|
-
EOF
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
4. **Seed a starter `.eikon`** so the tab and gallery have something to show before the user's first Ctrl+S. If `chafa` + `ffmpeg` are installed:
|
|
40
|
-
```bash
|
|
41
|
-
ffmpeg -hide_banner -loglevel error -i ~/.hermes/eikons/$NAME/source/base.* \
|
|
42
|
-
-vf "crop=min(iw\,ih):min(iw\,ih),eq=contrast=1.0" -frames:v 1 -f image2pipe -vcodec png - \
|
|
43
|
-
| chafa --size=48x24 --format=symbols --stretch --symbols=braille --colors=none --invert --preprocess off - \
|
|
44
|
-
> /tmp/eikon-frame.txt
|
|
45
|
-
```
|
|
46
|
-
Then write six copies as NDJSON:
|
|
47
|
-
```bash
|
|
48
|
-
python3 - <<'PY'
|
|
49
|
-
import json,os
|
|
50
|
-
n=os.environ["NAME"]; f=open("/tmp/eikon-frame.txt").read().rstrip("\n")
|
|
51
|
-
rows=(f.split("\n")+[""]*24)[:24]; data="\n".join(r.ljust(48)[:48] for r in rows)
|
|
52
|
-
out=[json.dumps({"eikon":1,"name":n,"width":48,"height":24,"author":os.environ.get("USER","")})]
|
|
53
|
-
for s in ["idle","listening","thinking","speaking","working","error"]:
|
|
54
|
-
out+=[json.dumps({"state":s,"fps":12,"frame_count":1,"loop_from":1}),json.dumps({"f":0,"data":data})]
|
|
55
|
-
open(os.path.expanduser(f"~/.hermes/eikons/{n}/{n}.eikon"),"w").write("\n".join(out)+"\n")
|
|
56
|
-
PY
|
|
57
|
-
```
|
|
58
|
-
If chafa/ffmpeg aren't installed, skip this step — the tab's `native` rasterizer will render on first open.
|
|
59
|
-
|
|
60
|
-
5. **Point the sidebar at it** (optional — user can do this from Gallery):
|
|
61
|
-
```bash
|
|
62
|
-
python3 -c "import json,os; p=os.path.expanduser('~/.hermes/herm/tui.json'); d=json.load(open(p)) if os.path.exists(p) else {}; d['eikon']='$NAME'; open(p,'w').write(json.dumps(d,indent=2))"
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
6. **Coach.** Tell the user:
|
|
66
|
-
> Wrote `<name>`. Open the **Eikon** tab (Alt+5 or type `/eikon`). Drag the preview to reframe, scroll to zoom. If it's too dark, nudge **contrast** up in the Knobs panel; if detail is mushy, try **symbols → block**. Tab cycles panes; ←→ on a knob row adjusts it. **Ctrl+S** saves and the sidebar updates.
|
|
67
|
-
|
|
68
|
-
## Knob vocabulary (for coaching — chafa rasterizer)
|
|
69
|
-
|
|
70
|
-
- `symbols`: braille · block · ascii · sextant · quad · half · wedge
|
|
71
|
-
- `fill`: none · stipple · ascii · braille (gradient class, layers under symbols)
|
|
72
|
-
- `dither`: none · ordered · diffusion · noise (diffusion = halftone grain)
|
|
73
|
-
- `invert`: on/off (most photos want on against a dark terminal)
|
|
74
|
-
- `flip`: none · h · v · hv
|
|
75
|
-
- `contrast`: 0.5–3.0
|
|
76
|
-
|
|
77
|
-
Spatial (preview pane, not knob rows): wheel zooms; zoom/pan x/pan y sliders below the frame.
|
|
78
|
-
|
|
79
|
-
## Don't
|
|
80
|
-
|
|
81
|
-
- Write a WIP file or poll for `tool.complete` — that flow is gone.
|
|
82
|
-
- Write tonal knob values into `studio.json.base` beyond `{}` — user owns tuning.
|
|
83
|
-
- Re-run the pipeline to "adjust contrast" for the user — tell them which knob to move.
|
|
55
|
+
Preview-only; Studio's output will differ (it tone-maps first).
|
|
56
|
+
|
|
57
|
+
## Don'ts
|
|
58
|
+
|
|
59
|
+
- Don't hand-write `.eikon` NDJSON.
|
|
60
|
+
- Don't pick knob values for the user. Name the knob.
|
|
61
|
+
- Don't repeat the 48×24 brief.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: eikon-create
|
|
3
|
+
description: Interactively generate source images (and optionally short videos) for a herm eikon avatar, iterate on them with the user, and land them under ~/.hermes/eikons/<name>/source/ so the Eikon Studio tab can tune and bake them. This is the agent-driven counterpart to Studio's one-shot Generate dialog.
|
|
4
|
+
tags: [eikon, avatar, image-gen, herm]
|
|
5
|
+
related_skills: [eikon]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /eikon-create — generate eikon source files interactively
|
|
9
|
+
|
|
10
|
+
You are producing **source files**, not the packed `.eikon`. Studio owns
|
|
11
|
+
crop, tone (contrast/invert/flip), rasterizer choice, and baking. Your
|
|
12
|
+
deliverable is one or more images/videos under
|
|
13
|
+
|
|
14
|
+
~/.hermes/eikons/<name>/source/
|
|
15
|
+
|
|
16
|
+
named so Studio resolves them: `base.*` for the default pose, `<state>.*`
|
|
17
|
+
for per-state overrides (`idle listening thinking speaking working
|
|
18
|
+
error`). Extensions: `png jpg jpeg webp gif bmp mp4 webm mov mkv`.
|
|
19
|
+
|
|
20
|
+
## What survives rasterization
|
|
21
|
+
|
|
22
|
+
Output is 48×24, one theme color, braille/block glyphs. The one-line
|
|
23
|
+
brief (say once, don't repeat):
|
|
24
|
+
|
|
25
|
+
> 48×24, one color. Best results: light subject on black background,
|
|
26
|
+
> high contrast, strong silhouette.
|
|
27
|
+
|
|
28
|
+
Outline carries everything — facial detail, fine texture, and tonal
|
|
29
|
+
gradients mostly vanish.
|
|
30
|
+
|
|
31
|
+
## What Studio fixes for you (don't regenerate for these)
|
|
32
|
+
|
|
33
|
+
Studio adjusts these live on any source without a new generation:
|
|
34
|
+
|
|
35
|
+
- **Aspect / framing** — Studio crops to a square window; zoom + pan
|
|
36
|
+
pick which square. A 16:9 or portrait source is fine.
|
|
37
|
+
- **Contrast** — slider, mean-centered. Flat or over-bright sources
|
|
38
|
+
are usually salvageable.
|
|
39
|
+
- **Invert** — light↔dark swap. A dark-subject-on-light source works
|
|
40
|
+
with invert off.
|
|
41
|
+
- **Flip** — horizontal/vertical mirror.
|
|
42
|
+
|
|
43
|
+
So: regenerate for **subject, pose, silhouette, background clutter**.
|
|
44
|
+
Don't regenerate for **crop, exposure, polarity, orientation**.
|
|
45
|
+
|
|
46
|
+
## Flow
|
|
47
|
+
|
|
48
|
+
### 0. Name
|
|
49
|
+
|
|
50
|
+
If the user passed an argument (`/eikon-create <name>`), slug it
|
|
51
|
+
(lowercase, `[^a-z0-9-]` → `-`, collapse runs, trim). Otherwise ask
|
|
52
|
+
once. Make the folder immediately so Studio's Open picker lists it:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
n="<slug>"; mkdir -p "${HERMES_HOME:-$HOME/.hermes}/eikons/$n/source"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 1. Subject
|
|
59
|
+
|
|
60
|
+
Ask what the eikon is. One line is enough. If they drop an image
|
|
61
|
+
instead of describing one, skip to §3 adopt-only.
|
|
62
|
+
|
|
63
|
+
### 2. Generate base
|
|
64
|
+
|
|
65
|
+
Call `image_generate` with the subject on line 1 and the fixed suffix
|
|
66
|
+
on line 2 — same suffix Studio seeds:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
<subject>
|
|
70
|
+
high contrast, light subject on dark, black background
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Prefer square if the tool takes `aspect_ratio`; if it doesn't, don't
|
|
74
|
+
worry — Studio crops. Show the result inline with ``
|
|
75
|
+
and a 48-wide terminal preview:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
chafa --size=48x24 --symbols=braille --colors=none --format=symbols --stretch "<path>" 2>/dev/null || true
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Ask: **keep, regenerate, or adjust?** On adjust, fold their note into
|
|
82
|
+
the subject line (leave the suffix alone). Loop. If two rounds fail on
|
|
83
|
+
background clutter, silently append `, isolated on pure black, no
|
|
84
|
+
floor, no environment` and try again.
|
|
85
|
+
|
|
86
|
+
### 3. Adopt
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
src="<path-or-downloaded-tmp>"
|
|
90
|
+
dst="${HERMES_HOME:-$HOME/.hermes}/eikons/$n/source/base.${src##*.}"
|
|
91
|
+
cp "$src" "$dst" && ls -l "$dst"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
URL return → `curl -fsSL -o /tmp/<n>-base.png "<url>"` first. Ambiguous
|
|
95
|
+
extension → `.png`.
|
|
96
|
+
|
|
97
|
+
### 4. Per-state sources (optional)
|
|
98
|
+
|
|
99
|
+
Base covers all six states by default. If the user wants distinct
|
|
100
|
+
ones, repeat §2–3 per state, saving as `<state>.<ext>`. Nudge the
|
|
101
|
+
subject line with pose intent:
|
|
102
|
+
|
|
103
|
+
| state | pose nudge |
|
|
104
|
+
|---|---|
|
|
105
|
+
| listening | head turned slightly toward viewer |
|
|
106
|
+
| thinking | head tilted back, contemplative |
|
|
107
|
+
| speaking | mouth open mid-word |
|
|
108
|
+
| working | head bowed forward |
|
|
109
|
+
| error | recoiling, startled |
|
|
110
|
+
|
|
111
|
+
### 5. Video (optional, only if asked)
|
|
112
|
+
|
|
113
|
+
Use `video_generate` if available. What matters for an eikon source:
|
|
114
|
+
|
|
115
|
+
- **Duration** — 2–4 s is the useful range. Longer just inflates the
|
|
116
|
+
bake. If the provider picks duration itself, accept whatever lands.
|
|
117
|
+
- **Aspect** — prefer 1:1 if the provider exposes it; otherwise any,
|
|
118
|
+
Studio crops.
|
|
119
|
+
- **Resolution** — low is fine; the target is 48×24. Don't ask for
|
|
120
|
+
HD if the provider lets you pick.
|
|
121
|
+
- **Start/end frame** — if the provider accepts a start (and/or end)
|
|
122
|
+
image, pass `base.*` so the clip anchors on the still pose and
|
|
123
|
+
loops cleanly. If it only takes text, that's fine — Studio plays
|
|
124
|
+
whatever frames it gets.
|
|
125
|
+
- **Loop** — nice-to-have, not required. If the provider has a loop
|
|
126
|
+
or seamless flag, set it; if not, don't chase it with prompting.
|
|
127
|
+
|
|
128
|
+
Providers vary in which of these knobs exist. **Pass what the tool
|
|
129
|
+
accepts, skip what it doesn't, and don't apologize for the gaps.**
|
|
130
|
+
None of them block a usable eikon.
|
|
131
|
+
|
|
132
|
+
Adopt as `<state>.mp4` (or `base.mp4` for an animated idle).
|
|
133
|
+
|
|
134
|
+
### 6. Hand off
|
|
135
|
+
|
|
136
|
+
Once source files are in place:
|
|
137
|
+
|
|
138
|
+
> Open the **Eikon** tab → `eikon` row → **<name>**. Tune zoom /
|
|
139
|
+
> contrast / invert / symbols there, then **Ctrl+S** to bake.
|
|
140
|
+
|
|
141
|
+
Stop there. Studio writes `<name>.eikon` and `studio.json`.
|
|
142
|
+
|
|
143
|
+
## Don'ts
|
|
144
|
+
|
|
145
|
+
- Don't write `.eikon` or `studio.json`.
|
|
146
|
+
- Don't pick contrast, invert, zoom, or rasterizer values — name the
|
|
147
|
+
knob, the user turns it in Studio.
|
|
148
|
+
- Don't regenerate for things Studio can fix (see table above).
|
|
149
|
+
- Don't enumerate provider capabilities to the user; just use what's
|
|
150
|
+
there.
|
|
151
|
+
- Don't repeat the 48×24 brief.
|
package/db.worker.js
CHANGED
|
@@ -35,7 +35,7 @@ var __create=Object.create;var{getPrototypeOf:__getProtoOf,defineProperty:__defP
|
|
|
35
35
|
JOIN sessions s ON s.id = m.session_id
|
|
36
36
|
WHERE messages_fts MATCH ?
|
|
37
37
|
ORDER BY rank LIMIT ?`)?.all(m,limit*4)??[],seen=new Set;return raw.filter((r)=>!seen.has(r.session_id)&&(seen.add(r.session_id),!0)).slice(0,limit)}finally{end()}}function rename(sid,title){let db=new Database(conn.path);try{return db.run("UPDATE sessions SET title = ? WHERE id = ?",[title,sid]),db.query("SELECT changes() AS c").get().c>0}finally{db.close()}}function remove(sid){let db=new Database(conn.path);try{if(!db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))return!1;return db.run("UPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?",[sid]),db.run("DELETE FROM messages WHERE session_id = ?",[sid]),db.run("DELETE FROM sessions WHERE id = ?",[sid]),!0}finally{db.close()}}var HERMES,SRC,conn,setHome=(h)=>{let next=`${h}/state.db`;if(conn.path===next)return;conn.path=SRC.file=next,resetDb()},stateDb=()=>{if(conn.ro)return conn.ro;try{return conn.ro=new Database(conn.path,{readwrite:!0,create:!1})}catch{return null}},resetDb=()=>{for(let s of stmts.values())s.finalize();stmts.clear(),conn.ro?.close(),conn.ro=null},stmts,q=(sql)=>{let db=stateDb();if(!db)return null;let s=stmts.get(sql);if(!s)stmts.set(sql,s=db.query(sql));return s},SUB=(c)=>`(p.ended_at IS NULL OR ${c}.started_at < p.ended_at)`,CONT=(c)=>`(p.end_reason = 'compression' AND ${c}.started_at >= p.ended_at)`,BR=(c)=>`(p.end_reason = 'branched' AND ${c}.started_at >= p.ended_at)`,kind=(parent,child)=>{if(!parent)return"root";if(parent.ended_at==null||child.started_at<parent.ended_at)return"subagent";if(parent.end_reason==="compression")return"continuation";if(parent.end_reason==="branched")return"branch";return"subagent"},COLS=`
|
|
38
|
-
s.id, s.source, s.model, s.started_at, s.ended_at, s.end_reason,
|
|
38
|
+
s.id, s.source, s.model, s.billing_provider, s.started_at, s.ended_at, s.end_reason,
|
|
39
39
|
s.message_count, s.tool_call_count,
|
|
40
40
|
s.input_tokens, s.output_tokens,
|
|
41
41
|
s.cache_read_tokens, s.cache_write_tokens, s.reasoning_tokens,
|
|
@@ -48,7 +48,7 @@ var __create=Object.create;var{getPrototypeOf:__getProtoOf,defineProperty:__defP
|
|
|
48
48
|
(SELECT MAX(timestamp) FROM messages WHERE session_id = s.id) AS last_active,
|
|
49
49
|
(SELECT COUNT(*) FROM sessions c
|
|
50
50
|
WHERE c.parent_session_id = s.id
|
|
51
|
-
AND (s.ended_at IS NULL OR c.started_at < s.ended_at)) AS subagent_count`,toRow=(r,lineage=null)=>({source:SRC,id:r.id,sessionSource:r.source,model:r.model,started_at:r.started_at,ended_at:r.ended_at,end_reason:r.end_reason,message_count:r.message_count,tool_call_count:r.tool_call_count,input_tokens:r.input_tokens,output_tokens:r.output_tokens,cache_read_tokens:r.cache_read_tokens,cache_write_tokens:r.cache_write_tokens,reasoning_tokens:r.reasoning_tokens,estimated_cost_usd:r.estimated_cost_usd,title:r.title,lastMessage:r.lastMessage,last_active:r.last_active,parent_session_id:r.parent_session_id,subagent_count:r.subagent_count,lineage_root_id:lineage}),one=(id)=>q(`SELECT ${COLS} FROM sessions s WHERE s.id = ?`)?.get(id)??null,byId=(id)=>{let r=one(id);return r?toRow(r):null},lastReal=()=>{let hit=q(`
|
|
51
|
+
AND (s.ended_at IS NULL OR c.started_at < s.ended_at)) AS subagent_count`,toRow=(r,lineage=null)=>({source:SRC,id:r.id,sessionSource:r.source,model:r.model,billing_provider:r.billing_provider,started_at:r.started_at,ended_at:r.ended_at,end_reason:r.end_reason,message_count:r.message_count,tool_call_count:r.tool_call_count,input_tokens:r.input_tokens,output_tokens:r.output_tokens,cache_read_tokens:r.cache_read_tokens,cache_write_tokens:r.cache_write_tokens,reasoning_tokens:r.reasoning_tokens,estimated_cost_usd:r.estimated_cost_usd,title:r.title,lastMessage:r.lastMessage,last_active:r.last_active,parent_session_id:r.parent_session_id,subagent_count:r.subagent_count,lineage_root_id:lineage}),one=(id)=>q(`SELECT ${COLS} FROM sessions s WHERE s.id = ?`)?.get(id)??null,byId=(id)=>{let r=one(id);return r?toRow(r):null},lastReal=()=>{let hit=q(`
|
|
52
52
|
SELECT s.id FROM sessions s
|
|
53
53
|
LEFT JOIN sessions p ON p.id = s.parent_session_id
|
|
54
54
|
WHERE s.source IN ('tui', 'cli') AND s.message_count > 0
|