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.
@@ -1,83 +1,61 @@
1
1
  ---
2
2
  name: eikon
3
- description: Author a sidebar eikon avatar produce/adopt a source image into ~/.hermes/eikons/<name>/, seed studio.json, then coach the user on the in-app Eikon tab controls. Filesystem is the API; no WIP files, no event polling.
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
- # Eikon author a herm sidebar avatar
7
+ # Building an eikon in herm
7
8
 
8
- ## When this fires
9
+ An eikon is a 48×24 monochrome text avatar. It lives on disk as:
9
10
 
10
- User asks for a new avatar, to "make an eikon", to turn an image into their sidebar avatar, or references `~/.hermes/eikons/`.
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
- ## Folder contract
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
- Herm's Eikon tab reads `studio.json` on open and re-renders from `source/`. Ctrl+S in the tab rewrites both files. The sidebar follows `eikon` (a bare name) in `~/.hermes/herm/tui.json`, resolved against `<profile>/eikons/` then bundled.
22
-
23
- ## Steps
24
-
25
- 1. **Get a source image.** User-supplied path → use as-is. Otherwise `image_generate` a square mono portrait (prompt should specify: high contrast, centered subject, plain background, 1:1). Save to a temp path.
26
-
27
- 2. **Pick a name.** Slugify what the user asked for (`lowercase, [a-z0-9-]`). Default `custom` if unclear.
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 `![base](<path>)`
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