herm-tui 1.9.0-dev.9 → 1.9.0

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,7 +1,7 @@
1
1
  ---
2
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]
3
+ description: Use when generating or adopting source images/videos for a Herm eikon so Eikon Studio can tune, bake, and optionally share it.
4
+ tags: [eikon, avatar, image-gen, video-gen, herm]
5
5
  related_skills: [eikon]
6
6
  ---
7
7
 
@@ -9,47 +9,52 @@ related_skills: [eikon]
9
9
 
10
10
  You are producing **source files**, not the packed `.eikon`. Studio owns
11
11
  crop, tone (contrast/invert/flip), rasterizer choice, and baking. Your
12
- deliverable is one or more images/videos under
12
+ deliverable is one or more images/videos under the active Hermes profile:
13
13
 
14
14
  ~/.hermes/eikons/<name>/source/
15
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`.
16
+ Studio resolves `base.*` for the default source and `<state>.*` for
17
+ per-state overrides (`idle listening thinking speaking working error`).
18
+ Direct `source/` discovery supports `png jpg jpeg webp gif bmp mp4 webm mov
19
+ mkv`; Studio's Local file picker currently accepts `png jpg jpeg webp gif mp4
20
+ webm mov`, so copy `bmp`/`mkv` into `source/` manually if needed.
21
+
22
+ Studio bakes with **Ctrl+S** without activation; **Ctrl+U** bakes and uses the
23
+ eikon as the active avatar.
19
24
 
20
25
  ## What survives rasterization
21
26
 
22
- Output is 48×24, one theme color, braille/block glyphs. The one-line
23
- brief (say once, don't repeat):
27
+ Output is 48×24, one theme color, braille/block glyphs. The one-line brief
28
+ (say once, don't repeat):
24
29
 
25
- > 48×24, one color. Best results: light subject on black background,
26
- > high contrast, strong silhouette.
30
+ > 48×24, one color. Best results: light subject on black background, high
31
+ > contrast, strong silhouette.
27
32
 
28
- Outline carries everything — facial detail, fine texture, and tonal
29
- gradients mostly vanish.
33
+ Outline carries everything — facial detail, fine texture, and tonal gradients
34
+ mostly vanish.
30
35
 
31
36
  ## What Studio fixes for you (don't regenerate for these)
32
37
 
33
38
  Studio adjusts these live on any source without a new generation:
34
39
 
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.
40
+ - **Aspect / framing** — Studio crops to a square window; zoom + pan pick
41
+ which square. A 16:9 or portrait source is fine.
42
+ - **Contrast** — slider, mean-centered. Flat or over-bright sources are
43
+ usually salvageable.
44
+ - **Invert** — light↔dark swap. A dark-subject-on-light source works with
45
+ invert off.
41
46
  - **Flip** — horizontal/vertical mirror.
42
47
 
43
- So: regenerate for **subject, pose, silhouette, background clutter**.
44
- Don't regenerate for **crop, exposure, polarity, orientation**.
48
+ So: regenerate for **subject, pose, silhouette, background clutter**. Don't
49
+ regenerate for **crop, exposure, polarity, orientation**.
45
50
 
46
51
  ## Flow
47
52
 
48
53
  ### 0. Name
49
54
 
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:
55
+ If the user passed an argument (`/eikon-create <name>`), slug it (lowercase,
56
+ `[^a-z0-9-]` → `-`, collapse runs, trim). Otherwise ask once. Make the folder
57
+ immediately so Studio's picker lists it:
53
58
 
54
59
  ```bash
55
60
  n="<slug>"; mkdir -p "${HERMES_HOME:-$HOME/.hermes}/eikons/$n/source"
@@ -57,52 +62,67 @@ n="<slug>"; mkdir -p "${HERMES_HOME:-$HOME/.hermes}/eikons/$n/source"
57
62
 
58
63
  ### 1. Subject
59
64
 
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.
65
+ Ask what the eikon is. One line is enough. If they drop an image instead of
66
+ describing one, skip to §3 adopt-only.
62
67
 
63
68
  ### 2. Generate base
64
69
 
65
- Call `image_generate` with the subject on line 1 and the fixed suffix
66
- on line 2 — same suffix Studio seeds:
70
+ Call `image_generate` with the subject on line 1 and Studio's fixed style hint
71
+ on line 2:
67
72
 
68
- ```
73
+ ```text
69
74
  <subject>, close-up portrait emphasizing the face/head, looking slightly left, stark black and white, bold silhouette, simple uncluttered shape
70
75
  high contrast, light subject on dark, black background
71
76
  ```
72
77
 
73
- Keep this general: replace `face/head` with the subject's most readable
74
- feature if it is not a character or creature. Prefer square if the tool
75
- takes `aspect_ratio`; if it doesn't, don't worry — Studio crops. Show
76
- the result inline with `![base](<path>)`
77
- and a 48-wide terminal preview:
78
+ Keep this general: replace `face/head` with the subject's most readable feature
79
+ if it is not a character or creature. Prefer `aspect_ratio="square"` if the
80
+ tool takes it; if it doesn't, don't worry — Studio crops.
81
+
82
+ After every candidate, adopt it into `source/base.<ext>` before handoff or
83
+ iteration. Show the result inline with `![base](<path>)` and a 48-wide terminal
84
+ preview:
78
85
 
79
86
  ```bash
80
87
  chafa --size=48x24 --symbols=braille --colors=none --format=symbols --stretch "<path>" 2>/dev/null || true
81
88
  ```
82
89
 
83
- Ask: **keep, regenerate, or adjust?** On adjust, fold their note into
84
- the subject line (leave the suffix alone). Loop. **Always overwrite the
85
- same `<state>.<ext>` in `source/` on every iteration** Studio reads
86
- that path live, so a new candidate that lives only in cache is invisible
87
- to the user. If two rounds fail on background clutter, silently append
88
- `, isolated on pure black, no floor, no environment` and try again.
90
+ Ask: **keep, regenerate, or adjust?** On adjust, fold their note into the
91
+ subject line and leave the style hint alone. If two rounds fail on background
92
+ clutter, silently append `, isolated on pure black, no floor, no environment`
93
+ and try again.
94
+
95
+ Overwrite the same role file (`base.<ext>` for the default source,
96
+ `<state>.<ext>` for overrides) so candidates do not drift across filenames. If
97
+ Studio is already open after an external write, tell the user to press reload
98
+ (`r`) or reopen the eikon; dirty drafts must be saved or reverted before reload.
99
+ A candidate left only in temp/cache or in an unselected filename is invisible.
89
100
 
90
101
  ### 3. Adopt
91
102
 
103
+ Use this for generated files, downloaded URLs, or a user-supplied local path.
104
+ Set `role=<state>` for per-state overrides; default is `base`.
105
+
92
106
  ```bash
107
+ role="${role:-base}"
93
108
  src="<path-or-downloaded-tmp>"
94
- dst="${HERMES_HOME:-$HOME/.hermes}/eikons/$n/source/base.${src##*.}"
109
+ base="${src##*/}"
110
+ ext="${base##*.}"
111
+ [ "$ext" = "$base" ] && ext="png"
112
+ ext=".${ext,,}"
113
+ case "$ext" in .png|.jpg|.jpeg|.webp|.gif|.bmp|.mp4|.webm|.mov|.mkv) ;; *) ext=".png" ;; esac
114
+ dst="${HERMES_HOME:-$HOME/.hermes}/eikons/$n/source/$role$ext"
95
115
  cp "$src" "$dst" && ls -l "$dst"
96
116
  ```
97
117
 
98
- URL return → `curl -fsSL -o /tmp/<n>-base.png "<url>"` first. Ambiguous
99
- extension → `.png`.
118
+ URL return → download to `/tmp/<n>-<role>.<ext>` first, preserving the media
119
+ extension when known. Ambiguous image URL → `.png`; ambiguous video URL → `.mp4`.
100
120
 
101
121
  ### 4. Per-state sources (optional)
102
122
 
103
- Base covers all six states by default. If the user wants distinct
104
- ones, repeat §2–3 per state, saving as `<state>.<ext>`. Nudge the
105
- subject line with pose intent:
123
+ Base covers all six states by default. If the user wants distinct ones, repeat
124
+ §2–3 per state, saving as `<state>.<ext>`. Nudge the subject line with pose
125
+ intent:
106
126
 
107
127
  | state | pose nudge |
108
128
  |---|---|
@@ -114,24 +134,39 @@ subject line with pose intent:
114
134
 
115
135
  ### 5. Video (optional, only if asked)
116
136
 
117
- Use `video_generate` if available. What matters for an eikon source:
118
-
119
- - **Duration** 2–4 s is the useful range. Longer just inflates the
120
- bake. If the provider picks duration itself, accept whatever lands.
121
- - **Aspect** — prefer 1:1 if the provider exposes it; otherwise any,
122
- Studio crops.
123
- - **Resolution** low is fine; the target is 48×24. Don't ask for
124
- HD if the provider lets you pick.
125
- - **Start/end frame** — if the provider accepts a start (and/or end)
126
- image, pass `base.*` so the clip anchors on the still pose and
127
- loops cleanly. If it only takes text, that's fine Studio plays
128
- whatever frames it gets.
129
- - **Loop** nice-to-have, not required. If the provider has a loop
130
- or seamless flag, set it; if not, don't chase it with prompting.
131
-
132
- Providers vary in which of these knobs exist. **Pass what the tool
133
- accepts, skip what it doesn't, and don't apologize for the gaps.**
134
- None of them block a usable eikon.
137
+ Use `video_generate` if available. Studio's Generate video row appears only
138
+ when the installed Hermes Agent video backend passes its requirement check; this
139
+ skill may also use any available agent `video_generate` tool. If video generation
140
+ is unavailable, adopt a local file or URL result instead.
141
+
142
+ Before prompting motion for a character/creature, establish a one-line persona:
143
+ temperament, bearing, and what an "idle" moment means for this subject. It keeps
144
+ motion specific instead of generic breathing/bobbing.
145
+
146
+ Motion discipline: animate only what the user asked for. Do not add ambient
147
+ breathing, swaying, shimmer, cloth ripple, or body motion unless requested;
148
+ small artifacts become obvious at 48×24. Prefer one deliberate motion plus one
149
+ blink over many tiny movements. Put unwanted motion in the negative prompt or
150
+ prompt body when the provider supports it.
151
+
152
+ What matters for an eikon video source:
153
+
154
+ - **Duration** 1–4 s; Studio defaults to 2 s. Longer usually inflates the
155
+ bake without improving readability.
156
+ - **Aspect** — prefer 1:1 if accepted; otherwise use an accepted aspect and let
157
+ Studio crop.
158
+ - **Resolution** — low is fine; the target is 48×24. Don't ask for HD.
159
+ - **Start image** — if the tool accepts it, pass `base.*` / the effective idle
160
+ source as `image_url` so frame 0 anchors on the still pose. Studio's current
161
+ bridge does not provide an end-frame handoff; skip unsupported knobs.
162
+ - **Loop** — useful but not required. If the provider has loop/seamless flags,
163
+ set them; otherwise do not chase a perfect loop by prompting.
164
+ - **Model tier** — if a lite/fast tier warps the subject, switch to a better
165
+ tier if the tool exposes `model=`. Don't burn iterations fighting artifacts a
166
+ tier bump fixes.
167
+
168
+ Providers vary. **Pass what the tool accepts, skip what it doesn't, and don't
169
+ apologize for the gaps.** None of them block a usable eikon.
135
170
 
136
171
  Adopt as `<state>.mp4` (or `base.mp4` for an animated idle).
137
172
 
@@ -139,17 +174,19 @@ Adopt as `<state>.mp4` (or `base.mp4` for an animated idle).
139
174
 
140
175
  Once source files are in place:
141
176
 
142
- > Open the **Eikon** tab → `eikon` row → **<name>**. Tune zoom /
143
- > contrast / invert / symbols there, then **Ctrl+S** to bake.
177
+ > Open **Eikon → Studio** → `eikon` row → **<name>**. Tune zoom / contrast /
178
+ > invert / symbols there, then **Ctrl+S** to bake or **Ctrl+U** to bake and use.
179
+ > After a saved bake, press `u` in Studio for the Submit eikon dialog or `s` in
180
+ > **Library** for Share to catalog; Herm previews the bundle and GitHub PR target
181
+ > before submission.
144
182
 
145
183
  Stop there. Studio writes `<name>.eikon` and `studio.json`.
146
184
 
147
185
  ## Don'ts
148
186
 
149
187
  - Don't write `.eikon` or `studio.json`.
150
- - Don't pick contrast, invert, zoom, or rasterizer values — name the
151
- knob, the user turns it in Studio.
188
+ - Don't pick contrast, invert, zoom, or rasterizer values — name the knob, the
189
+ user turns it in Studio.
152
190
  - Don't regenerate for things Studio can fix (see table above).
153
- - Don't enumerate provider capabilities to the user; just use what's
154
- there.
191
+ - Don't enumerate provider capabilities to the user; just use what's there.
155
192
  - Don't repeat the 48×24 brief.
package/db.worker.js CHANGED
@@ -22,12 +22,15 @@ var __create=Object.create;var{getPrototypeOf:__getProtoOf,defineProperty:__defP
22
22
  ORDER BY c.started_at DESC LIMIT 1`)?.get(sid);return{...pred&&{continuesFrom:pred},...succ&&{compressedTo:succ}}}finally{end()}}function tip(sid){let step=q(`SELECT c.id FROM sessions c
23
23
  JOIN sessions p ON p.id = c.parent_session_id
24
24
  WHERE p.id = ? AND ${CONT("c")}
25
- ORDER BY c.started_at DESC LIMIT 1`),cur=sid;for(let i=0;i<100;i++){let next=step?.get(cur);if(!next)return cur;cur=next.id}return cur}function peek(sid,n=60){let end=mark("io:sessions.peek");try{let ext=[hasMsgCol("platform_message_id")?"platform_message_id":"NULL AS platform_message_id",hasMsgCol("observed")?"observed":"NULL AS observed"];return(q(`SELECT role, SUBSTR(content,1,400) AS content, tool_name,
25
+ ORDER BY c.started_at DESC LIMIT 1`),cur=sid;for(let i=0;i<100;i++){let next=step?.get(cur);if(!next)return cur;cur=next.id}return cur}function peek(sid,_n=4){let end=mark("io:sessions.peek");try{let ext=[hasMsgCol("platform_message_id")?"platform_message_id":"NULL AS platform_message_id",hasMsgCol("observed")?"observed":"NULL AS observed"];return(q(`SELECT role, SUBSTR(content,1,400) AS content, tool_name,
26
26
  SUBSTR(tool_calls,1,400) AS tool_calls, timestamp AS at,
27
27
  ${ext.join(", ")}
28
- FROM (SELECT * FROM messages WHERE session_id = ?
29
- ORDER BY id DESC LIMIT ?)
30
- ORDER BY id ASC`)?.all(sid,n)??[]).map((r)=>({role:r.role,content:r.content,tool_name:r.tool_name,tool_calls:r.tool_calls,...r.platform_message_id!==null&&{platform_message_id:r.platform_message_id},...r.observed!==null&&{observed:r.observed},at:r.at}))}finally{end()}}function goalState(sid){let row=q("SELECT value FROM state_meta WHERE key = ?")?.get(`goal:${sid}`);if(!row)return null;try{let j=JSON.parse(row.value),checklist=(Array.isArray(j.checklist)?j.checklist:[]).map(parseItem).filter((x)=>x!==null),subgoals=(Array.isArray(j.subgoals)?j.subgoals:[]).map((s)=>typeof s==="string"?s.trim():"").filter((s)=>s.length>0);return{goal:String(j.goal??""),status:j.status??"active",turn_count:typeof j.turn_count==="number"?j.turn_count:void 0,max_turns:j.max_turns??null,checklist:checklist.length>0?checklist:void 0,subgoals:subgoals.length>0?subgoals:void 0,decomposed:j.decomposed===!0?!0:void 0}}catch{return null}}function search(query,limit=30){let m=fts(query);if(!m)return[];let end=mark("io:sessions.search");try{let raw=q(`SELECT m.session_id, m.role,
28
+ FROM (
29
+ SELECT * FROM (SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT 2)
30
+ UNION
31
+ SELECT * FROM (SELECT * FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT 2)
32
+ )
33
+ ORDER BY id ASC`)?.all(sid,sid)??[]).map((r)=>({role:r.role,content:r.content,tool_name:r.tool_name,tool_calls:r.tool_calls,...r.platform_message_id!==null&&{platform_message_id:r.platform_message_id},...r.observed!==null&&{observed:r.observed},at:r.at}))}finally{end()}}function goalState(sid){let row=q("SELECT value FROM state_meta WHERE key = ?")?.get(`goal:${sid}`);if(!row)return null;try{let j=JSON.parse(row.value),checklist=(Array.isArray(j.checklist)?j.checklist:[]).map(parseItem).filter((x)=>x!==null),subgoals=(Array.isArray(j.subgoals)?j.subgoals:[]).map((s)=>typeof s==="string"?s.trim():"").filter((s)=>s.length>0);return{goal:String(j.goal??""),status:j.status??"active",turn_count:typeof j.turn_count==="number"?j.turn_count:void 0,max_turns:j.max_turns??null,checklist:checklist.length>0?checklist:void 0,subgoals:subgoals.length>0?subgoals:void 0,decomposed:j.decomposed===!0?!0:void 0}}catch{return null}}function search(query,limit=30){let m=fts(query);if(!m)return[];let end=mark("io:sessions.search");try{let raw=q(`SELECT m.session_id, m.role,
31
34
  snippet(messages_fts, 0, '>>>', '<<<', '...', 40) AS snippet,
32
35
  s.source, s.model, s.started_at,
33
36
  COALESCE(s.title, SUBSTR(m.content, 1, 120)) AS title
@@ -55,7 +58,7 @@ var __create=Object.create;var{getPrototypeOf:__getProtoOf,defineProperty:__defP
55
58
  WHERE s.source IN ('tui', 'cli') AND s.message_count > 0
56
59
  AND (s.parent_session_id IS NULL OR ${CONT("s")})
57
60
  ORDER BY s.started_at DESC LIMIT 1
58
- `)?.get();if(!hit)return;let row=byId(chainTip(hit.id));return row&&row.message_count>0?row:void 0},chainTip=(sid)=>tip(walkUp(sid)),systemPrompt=()=>q(`SELECT id, system_prompt AS text FROM sessions
61
+ `)?.get();if(!hit)return;return byId(chainTip(hit.id))??void 0},chainTip=(sid)=>tip(walkUp(sid)),systemPrompt=()=>q(`SELECT id, system_prompt AS text FROM sessions
59
62
  WHERE system_prompt IS NOT NULL AND length(system_prompt) > 1000
60
63
  ORDER BY started_at DESC LIMIT 1`)?.get()??null,VALID_ITEM,parseItem=(raw)=>{if(!raw||typeof raw!=="object")return null;let o=raw,text=typeof o.text==="string"?o.text:"";if(!text.trim())return null;let s=typeof o.status==="string"?o.status:"pending",status=VALID_ITEM.has(s)?s:"pending",by=typeof o.added_by==="string"?o.added_by:void 0;return{text,status,addedBy:by==="judge"||by==="user"?by:void 0}},fts=(s)=>s.trim().split(/\s+/).filter(Boolean).map((w)=>/^\w+$/.test(w)?`${w}*`:`"${w.replace(/"/g,'""')}"`).join(" ");var init_sessions_db=__esm(()=>{init_perf();init_sessions_db();HERMES=process.env.HERMES_HOME||`${process.env.HOME||homedir()}/.hermes`,SRC={file:`${HERMES}/state.db`,relative:"state.db",label:"state.db"},conn={path:SRC.file,ro:null},stmts=new Map,msgCols=new Map;VALID_ITEM=new Set(["pending","completed","impossible"])});function analytics(days,opts){let since=Math.floor(Date.now()/1000)-days*86400,db=stateDb();if(!db)return ZERO;let q2=db.query.bind(db),tot=q2(`SELECT COUNT(*) n,
61
64
  COALESCE(SUM(message_count),0) msgs,