viberoom 0.7.0 → 0.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.
package/NOTICE CHANGED
@@ -27,9 +27,11 @@ This product bundles the following third-party material:
27
27
  - Cascadia Code: Copyright 2019 Microsoft Corporation.
28
28
 
29
29
  - Vendor icons (assets/vendors/): the logos of Claude, Codex, Gemini, Cursor,
30
- OpenCode and GitHub Copilot, as published in the Agent Client Protocol
31
- registry (https://agentclientprotocol.com). Each mark belongs to its owner and
32
- is used only to identify the agent it stands for.
30
+ OpenCode, GitHub Copilot and Grok, as published in the Agent Client Protocol
31
+ registry (https://agentclientprotocol.com), and the Nous Research mark for
32
+ Hermes Agent, traced in one colour from the icon published on nousresearch.com.
33
+ Each mark belongs to its owner and is used only to identify the agent it
34
+ stands for.
33
35
 
34
36
  - ACP adapters (vendor/acp/, copied from the npm packages at build time by
35
37
  scripts/vendor-acp.mjs, each with its licence file):
package/README.md CHANGED
@@ -14,16 +14,86 @@ Open a room, summon the agents you already have, give each one a role, and let t
14
14
  </p>
15
15
 
16
16
  <p align="center">
17
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="A room with two vibemates: one explains a git command from a screenshot, the other adds the part that changes whose problem it is">
17
+ <a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html"><img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/looks.gif" width="960" alt="The recorded session, drawn in each of the six looks in turn; click to open the interactive demo"></a>
18
18
  </p>
19
19
 
20
20
  <p align="center">
21
- <b>Claude</b> &nbsp;·&nbsp; <b>Codex</b> &nbsp;·&nbsp; <b>Gemini</b> &nbsp;·&nbsp; <b>Cursor</b> &nbsp;·&nbsp; <b>OpenCode</b> &nbsp;·&nbsp; <b>GitHub Copilot</b><br>
21
+ <a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html"><img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/try-the-demo.png" width="880" alt="Try the interactive demo: a recorded session in the real viberoom window, two rooms, six vibemates, six looks, nothing to install"></a>
22
+ </p>
23
+
24
+ <p align="center">
25
+ <b>Claude</b> &nbsp;·&nbsp; <b>Codex</b> &nbsp;·&nbsp; <b>Gemini</b> &nbsp;·&nbsp; <b>Cursor</b> &nbsp;·&nbsp; <b>OpenCode</b> &nbsp;·&nbsp; <b>GitHub Copilot</b> &nbsp;·&nbsp; <b>Grok Build</b> &nbsp;·&nbsp; <b>Hermes Agent</b><br>
22
26
  <sub>whichever of them you have installed, with your own logins, in one room</sub>
23
27
  </p>
24
28
 
25
29
  <br>
26
30
 
31
+ ## See it before you install it
32
+
33
+ Two rooms, six vibemates, one afternoon: a small CLI fixed and tested by Claude, Codex and OpenCode, and a
34
+ bakery's launch planned by Claude, Codex and Gemini, with the tool calls, the pictures, the file previews and
35
+ the Mermaid diagrams as they happened. The session is recorded as pages that run the real viberoom window
36
+ with no hub behind it. Every card below opens it in that look: walk between the rooms, open the participants,
37
+ change the look under Settings. (The pages live in [`docs/demo/`](docs/demo/) too, for a copy of your own.)
38
+
39
+ <a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html"><img align="right" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-vibeclassic.jpg" alt="VibeClassic"></a>
40
+
41
+ **VibeClassic** &mdash; the default: lavender light, soft tiles, room to breathe.<br>
42
+ <a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic.html">Open the demo in VibeClassic &rarr;</a>
43
+
44
+ <br clear="all">
45
+
46
+ <a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic-dark.html"><img align="left" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-vibeclassic-dark.jpg" alt="VibeClassic Dark"></a>
47
+
48
+ **VibeClassic Dark** &mdash; the same room after dark: navy paper, the same tiles, nothing glares.<br>
49
+ <a href="https://todor-rusev.github.io/viberoom/demo/vibeclassic-dark.html">Open the demo in VibeClassic Dark &rarr;</a>
50
+
51
+ <br clear="all">
52
+
53
+ <a href="https://todor-rusev.github.io/viberoom/demo/clay.html"><img align="right" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-clay.jpg" alt="Clay"></a>
54
+
55
+ **Clay** &mdash; sand, terracotta and teal on warm paper; matte objects, soft corners.<br>
56
+ <a href="https://todor-rusev.github.io/viberoom/demo/clay.html">Open the demo in Clay &rarr;</a>
57
+
58
+ <br clear="all">
59
+
60
+ <a href="https://todor-rusev.github.io/viberoom/demo/eye-comfort.html"><img align="left" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-eye-comfort.jpg" alt="Eye Comfort"></a>
61
+
62
+ **Eye Comfort** &mdash; cream instead of white, dark grey instead of black, air between the lines: made for long evenings.<br>
63
+ <a href="https://todor-rusev.github.io/viberoom/demo/eye-comfort.html">Open the demo in Eye Comfort &rarr;</a>
64
+
65
+ <br clear="all">
66
+
67
+ <a href="https://todor-rusev.github.io/viberoom/demo/3d-clayful.html"><img align="right" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-3d-clayful.jpg" alt="3D clayful"></a>
68
+
69
+ **3D clayful** &mdash; cream, lavender, pink and mint with real volume: light from the top left, a wall under every object.<br>
70
+ <a href="https://todor-rusev.github.io/viberoom/demo/3d-clayful.html">Open the demo in 3D clayful &rarr;</a>
71
+
72
+ <br clear="all">
73
+
74
+ <a href="https://todor-rusev.github.io/viberoom/demo/terminal.html"><img align="left" width="460" src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/readme/card-terminal.jpg" alt="Terminal"></a>
75
+
76
+ **Terminal** &mdash; phosphor green on black, one monospace face, hairlines instead of shadows.<br>
77
+ <a href="https://todor-rusev.github.io/viberoom/demo/terminal.html">Open the demo in Terminal &rarr;</a>
78
+
79
+ <br clear="all">
80
+
81
+
82
+ ## Six looks, or your own
83
+
84
+ A look is how the window is drawn: colours, light, corners, fonts. Pick one under Settings, fine-tune it,
85
+ or ask a vibemate for "a warm paper look for long evenings": it reads every design token with what it
86
+ means, checks its draft (the hub measures whether the words read on their paper and reports the numbers,
87
+ since a vibemate cannot see colours), saves the look among your own with your name on it, and proposes
88
+ wearing it as a card you can try on first. Your looks are files: export one to share it, import one
89
+ someone sent you.
90
+
91
+ <p align="center">
92
+ <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/looks-picker.png" width="820" alt="Settings: the look picker with the six shipped looks">
93
+ </p>
94
+
95
+ <br>
96
+
27
97
  ## The idea
28
98
 
29
99
  One agent is a conversation. Three agents in one room are a **team**: one decides what "done" means,
@@ -44,7 +114,7 @@ effort and how much it may do on its own. Three vibemates from the same agent wi
44
114
  vibios behave like three different people, which is exactly the point.
45
115
 
46
116
  <p align="center">
47
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/summon.png" width="960" alt="Summon a vibemate: agent, model, effort, mode, name, face and a private brief">
117
+ <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/summon.png" width="960" alt="Summon a vibemate: eight agents to pick from, then a name, a face and a private brief">
48
118
  </p>
49
119
 
50
120
  <br>
@@ -59,8 +129,7 @@ vibemate gets the rules as instructions, so the room runs the way you wrote it d
59
129
  <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/rules.png" width="820" alt="Room rules with live @references to the vibemates">
60
130
  </p>
61
131
 
62
- Here is what a five-line ruleset buys you: a handoff chain that runs on its own, with the human at
63
- both ends.
132
+ Five lines of rules buy you a handoff chain that runs on its own, with the human at both ends.
64
133
 
65
134
  <p align="center">
66
135
  <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/art/handoff.svg" width="960" alt="One task, four hands: you to Pip to Nova to Rex, review loop, report back">
@@ -82,11 +151,10 @@ library marks it as theirs until you have read it.
82
151
 
83
152
  ## Let them design the room
84
153
 
85
- A vibemate can design a room as well as work in it. It has a built-in skill on what makes rules and
86
- roles good, and four hub tools: read the room's settings and rules, check a design and preview the
87
- brief the others would receive, save a template for you to pick under New room, or propose a change to
88
- the room you are in. A proposal is a card in the chat with the diff; nothing changes until you click
89
- Apply, and the room is told what you decided.
154
+ A vibemate can design a room as well as work in it: read the room's settings and rules, check a design
155
+ and preview the brief the others would receive, save a template for you to pick under New room, or propose
156
+ a change to the room you are in. A proposal is a card in the chat with the diff; nothing changes until you
157
+ click Apply, and the room is told what you decided.
90
158
 
91
159
  <br>
92
160
 
@@ -98,8 +166,8 @@ Apply, and the room is told what you decided.
98
166
 
99
167
  Write to the room and every vibemate answers, each after a short pause so replies do not trip over
100
168
  each other. `@Name` one of them and the rest read along. Vibemates talk to each other the same way:
101
- a reply wakes the others, `@Name` picks one, and a hop limit keeps an argument from running all night. **Hush** stops every running reply
102
- at once; the room stays quiet until you write again.
169
+ a reply wakes the others, `@Name` picks one, and a hop limit keeps an argument from running all night.
170
+ **Hush** stops every running reply at once; the room stays quiet until you write again.
103
171
 
104
172
  <br>
105
173
 
@@ -111,7 +179,7 @@ at that line. A diagram block becomes a diagram. Change one of your own messages
111
179
  the vibemates are told, or the conversation rewinds to that point.
112
180
 
113
181
  <p align="center">
114
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/home.png" width="960" alt="The home page: welcome back, your rooms, what you can do here">
182
+ <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="A room with two vibemates: one explains a git command from a screenshot, the other adds the part that changes whose problem it is">
115
183
  </p>
116
184
 
117
185
  <br>
@@ -141,10 +209,11 @@ it in a browser tab. Later, `npm install -g viberoom@latest` and the next start
141
209
  sees your keys; every agent keeps its own login.
142
210
  - **Edit a message.** Fix what you said; the vibemates get the memo, or the conversation rewinds.
143
211
  - **Quote a message.** Select a fragment of a bubble, or copy it with Ctrl+C, and it goes into your next
144
- message as a quote with who said it and when, so the vibemates read it as that person's words, not yours.
212
+ message as a quote, with who said it and when.
145
213
  - **Your messages on a timeline.** A thin strip on the chat's right edge, one mark per message of yours:
146
214
  hover for the message with its neighbours, click to jump there.
147
- - **Pick a folder from a tree.** Browse the machine's folders when a room needs one; make a new one on the spot.
215
+ - **Nothing is cut in silence.** A persona or a rule longer than the room's limit is refused with the
216
+ numbers, and the window offers to cut it, raise the limit, or go back.
148
217
  - **Settings save themselves.** Change a setting and it is saved: on Enter, on leaving the field, on a pick.
149
218
  - **Search the room.** Everything anyone said, one search box.
150
219
  - **For geeks.** Every panel folds its technical settings behind a toggle. You never have to open it.
@@ -154,11 +223,12 @@ it in a browser tab. Later, `npm install -g viberoom@latest` and the next start
154
223
  ## What you need
155
224
 
156
225
  - Node.js 22 or newer.
157
- - At least one coding agent installed and logged in: Claude Code, Codex, Gemini CLI, Cursor, OpenCode
158
- or GitHub Copilot. viberoom finds the ones you have and offers only those. It installs none of them.
226
+ - At least one coding agent installed and logged in: Claude Code, Codex, Gemini CLI, Cursor, OpenCode,
227
+ GitHub Copilot, Grok Build or Hermes Agent. viberoom finds the ones you have and offers only those. It
228
+ installs none of them.
159
229
  - A browser. Chrome, Edge or Brave for the app window; anything modern for a tab.
160
230
 
161
- Something does not start? `viberoom doctor` checks these four things and says which one is missing.
231
+ Something does not start? `viberoom doctor` checks these things and says which one is missing.
162
232
 
163
233
  <br>
164
234
 
@@ -175,7 +245,7 @@ viberoom logs the background hub's log
175
245
 
176
246
  `Enter` sends, `Shift+Enter` is a new line, `@Name` addresses one vibemate, `/name` invokes a skill.
177
247
  Your data lives in `~/.viberoom` (or `$VIBEROOM_DATA_DIR`): settings, rooms with their history,
178
- skills and the log.
248
+ skills, looks and the log.
179
249
 
180
250
  <br>
181
251
 
@@ -184,6 +254,7 @@ skills and the log.
184
254
  - A question ("how do I ...?") goes to [Discussions → Q&A](https://github.com/todor-rusev/viberoom/discussions/categories/q-a).
185
255
  - An idea goes to [Discussions → Ideas](https://github.com/todor-rusev/viberoom/discussions/categories/ideas).
186
256
  - A bug goes to [Issues](https://github.com/todor-rusev/viberoom/issues/new/choose); the form asks for what a fix needs.
257
+ - A change you would like to make: read [CONTRIBUTING.md](CONTRIBUTING.md) first. Pull requests are credited, not merged as they are.
187
258
 
188
259
  <br>
189
260
 
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
2
+ <path fill="currentColor" d="M0.58392448,14.9254204 L0.8326,14.66295 C2.004465,13.42985 3.1678,12.2082 2.45814,10.48105 C1.50813,8.1701 2.061355,5.4619 3.820575,3.70057 C5.6495,1.8709 8.3431,1.40957 10.59295,2.336505 C11.0907,2.52161 11.5245,2.785025 11.86295,3.02993 L9.98425,3.89849 C8.235,3.163775 6.23115,3.66355 5.0081,4.88809 C3.354105,6.5426 3.019895,9.4117 4.95835,11.2656 L-0.335,15.99995 C-0.066496,15.62975 0.2538896,15.275934 0.58392448,14.9254204 Z M14.0391,2.288155 L16.33165,2.57749377e-12 L16.20795,0.172288 C14.4658,2.574355 13.6153,3.749045 14.29795,6.6879 C14.76445,8.68415 14.261,10.90255 12.63545,12.53005 C10.5861,14.58325 7.3066,15.0403 4.6059,13.19215 L6.48885,12.3193 C8.2125,12.99705 10.0983,12.69945 11.4536,11.34255 C12.80895,9.9856 13.1133,8.00925 12.4321,6.3647 C12.30265,6.05285 11.9144,5.97455 11.64275,6.1753 L6.102,10.27035 L14.0391,2.288155 Z"/>
3
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 0 63 63"><path fill="currentColor" d="M25 0h10v1h-10zM28 1h10v1h-10zM19 2h4v1h-4zM30 2h10v1h-10zM16 3h10v1h-10zM32 3h10v1h-10zM14 4h14v1h-14zM33 4h10v1h-10zM12 5h17v1h-17zM34 5h10v1h-10zM11 6h19v1h-19zM35 6h10v1h-10zM9 7h22v1h-22zM35 7h11v1h-11zM8 8h21v1h-21zM30 8h2v1h-2zM36 8h10v1h-10zM7 9h20v1h-20zM31 9h1v1h-1zM37 9h7v1h-7zM6 10h19v1h-19zM26 10h1v1h-1zM30 10h3v1h-3zM37 10h6v1h-6zM47 10h1v1h-1zM5 11h7v1h-7zM13 11h11v1h-11zM30 11h2v1h-2zM38 11h1v1h-1zM41 11h1v1h-1zM47 11h2v1h-2zM8 12h4v1h-4zM13 12h4v1h-4zM21 12h1v1h-1zM27 12h6v1h-6zM38 12h1v1h-1zM41 12h1v1h-1zM43 12h2v1h-2zM46 12h3v1h-3zM9 13h2v1h-2zM27 13h7v1h-7zM38 13h11v1h-11zM12 14h1v1h-1zM15 14h8v1h-8zM27 14h7v1h-7zM39 14h11v1h-11zM3 15h4v1h-4zM9 15h26v1h-26zM39 15h11v1h-11zM3 16h32v1h-32zM36 16h15v1h-15zM3 17h32v1h-32zM36 17h15v1h-15zM2 18h49v1h-49zM2 19h50v1h-50zM2 20h17v1h-17zM20 20h32v1h-32zM2 21h17v1h-17zM20 21h32v1h-32zM2 22h18v1h-18zM21 22h31v1h-31zM2 23h11v1h-11zM14 23h6v1h-6zM21 23h32v1h-32zM2 24h11v1h-11zM17 24h2v1h-2zM29 24h24v1h-24zM3 25h8v1h-8zM19 25h3v1h-3zM30 25h23v1h-23zM3 26h7v1h-7zM18 26h6v1h-6zM30 26h23v1h-23zM4 27h6v1h-6zM11 27h1v1h-1zM20 27h3v1h-3zM24 27h1v1h-1zM30 27h7v1h-7zM41 27h12v1h-12zM5 28h2v1h-2zM8 28h5v1h-5zM20 28h2v1h-2zM24 28h1v1h-1zM30 28h24v1h-24zM9 29h4v1h-4zM21 29h1v1h-1zM30 29h24v1h-24zM9 30h2v1h-2zM12 30h1v1h-1zM30 30h24v1h-24zM9 31h3v1h-3zM30 31h24v1h-24zM9 32h2v1h-2zM30 32h25v1h-25zM9 33h2v1h-2zM30 33h25v1h-25zM9 34h1v1h-1zM29 34h26v1h-26zM8 35h2v1h-2zM29 35h26v1h-26zM8 36h2v1h-2zM29 36h27v1h-27zM8 37h2v1h-2zM29 37h27v1h-27zM8 38h3v1h-3zM12 38h4v1h-4zM29 38h27v1h-27zM8 39h3v1h-3zM13 39h1v1h-1zM16 39h1v1h-1zM29 39h27v1h-27zM8 40h4v1h-4zM28 40h6v1h-6zM35 40h22v1h-22zM8 41h4v1h-4zM28 41h6v1h-6zM35 41h18v1h-18zM54 41h3v1h-3zM8 42h5v1h-5zM28 42h6v1h-6zM35 42h18v1h-18zM54 42h3v1h-3zM7 43h6v1h-6zM28 43h5v1h-5zM35 43h18v1h-18zM55 43h2v1h-2zM0 44h1v1h-1zM7 44h7v1h-7zM28 44h5v1h-5zM34 44h19v1h-19zM55 44h3v1h-3zM0 45h1v1h-1zM7 45h7v1h-7zM28 45h25v1h-25zM55 45h3v1h-3zM0 46h1v1h-1zM7 46h9v1h-9zM19 46h4v1h-4zM28 46h6v1h-6zM36 46h17v1h-17zM55 46h3v1h-3zM0 47h2v1h-2zM6 47h18v1h-18zM28 47h5v1h-5zM36 47h17v1h-17zM55 47h3v1h-3zM60 47h1v1h-1zM0 48h3v1h-3zM6 48h19v1h-19zM28 48h4v1h-4zM36 48h17v1h-17zM54 48h4v1h-4zM60 48h1v1h-1zM0 49h9v1h-9zM10 49h16v1h-16zM28 49h25v1h-25zM54 49h4v1h-4zM60 49h1v1h-1zM1 50h7v1h-7zM9 50h43v1h-43zM53 50h5v1h-5zM60 50h1v1h-1zM0 51h1v1h-1zM2 51h5v1h-5zM9 51h18v1h-18zM29 51h29v1h-29zM59 51h2v1h-2zM0 52h2v1h-2zM8 52h18v1h-18zM30 52h27v1h-27zM59 52h2v1h-2zM1 53h3v1h-3zM6 53h19v1h-19zM27 53h1v1h-1zM31 53h26v1h-26zM59 53h2v1h-2zM2 54h9v1h-9zM12 54h11v1h-11zM27 54h1v1h-1zM32 54h25v1h-25zM58 54h2v1h-2zM4 55h6v1h-6zM12 55h12v1h-12zM25 55h2v1h-2zM28 55h1v1h-1zM33 55h23v1h-23zM57 55h3v1h-3zM6 56h1v1h-1zM11 56h13v1h-13zM26 56h1v1h-1zM28 56h1v1h-1zM32 56h4v1h-4zM38 56h3v1h-3zM46 56h1v1h-1zM49 56h6v1h-6zM56 56h3v1h-3zM6 57h2v1h-2zM9 57h16v1h-16zM27 57h1v1h-1zM30 57h3v1h-3zM36 57h3v1h-3zM47 57h2v1h-2zM50 57h8v1h-8zM7 58h6v1h-6zM14 58h11v1h-11zM28 58h3v1h-3zM35 58h3v1h-3zM49 58h1v1h-1zM51 58h6v1h-6zM8 59h4v1h-4zM14 59h15v1h-15zM34 59h3v1h-3zM50 59h1v1h-1zM52 59h3v1h-3zM13 60h15v1h-15zM34 60h2v1h-2zM50 60h4v1h-4zM12 61h11v1h-11zM24 61h1v1h-1zM33 61h2v1h-2zM51 61h2v1h-2zM12 62h10v1h-10zM23 62h2v1h-2zM30 62h1v1h-1zM33 62h1v1h-1zM52 62h1v1h-1z"/></svg>
@@ -10,7 +10,7 @@ export function loginState(probe, evidence) {
10
10
  return "unknown";
11
11
  return probe.files.length ? "missing" : "unknown";
12
12
  }
13
- const LOGIN_WORDS = /\b(not logged in|log ?in required|login required|please log ?in|unauthori[sz]ed|authentication (failed|required|error)|invalid api key|api key (is )?(missing|not set|invalid)|no credentials|credentials not found|401|403|oauth|token (expired|invalid))\b/i;
13
+ const LOGIN_WORDS = /\b(not logged in|log ?in required|login required|please log ?in|unauthori[sz]ed|authentication (failed|required|error)|invalid api key|api key (is )?(missing|not set|invalid)|no credentials|credentials not found|no (llm )?provider (configured|available)|401|403|oauth|token (expired|invalid))\b/i;
14
14
  const MISSING_WORDS = /\b(enoent|not found|no such file|is not recognized|command not found|spawn\w* (failed|error))\b/i;
15
15
  const TIMEOUT_WORDS = /\b(timed out|timeout|took too long|did not answer|no response)\b/i;
16
16
  export function classifyStartFailure(input) {
package/dist/hub.js CHANGED
@@ -11,6 +11,7 @@ import { DEFAULT_ROOM_SETTINGS } from "./persona.js";
11
11
  import { Room } from "./room.js";
12
12
  import { SkillLibrary } from "./skills.js";
13
13
  import { TemplateLibrary, roomSettingsFromTemplate } from "./templates.js";
14
+ import { LookLibrary, checkLookSpec, loadTokens } from "./looks.js";
14
15
  export const ADJUSTABLE = {
15
16
  canvas: "colour", panel: "colour", bubble: "colour", mine: "colour", ring: "colour", ink: "colour", muted: "colour", accent: "colour",
16
17
  face: "colour", logo: "colour", logoInk: "colour", corners: "scale",
@@ -21,11 +22,61 @@ export const DEFAULT_APPEARANCE = { chatFontSize: 14.5, font: "nunito", mono: "j
21
22
  export const DIAGRAM_PRESETS = ["pop", "lavender", "mint", "sunset", "slate"];
22
23
  const ROOM_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
23
24
  const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules"];
25
+ export function nextAppearance(current, a) {
26
+ const chatFontSize = Number(a.chatFontSize ?? current.chatFontSize ?? DEFAULT_APPEARANCE.chatFontSize);
27
+ if (!Number.isFinite(chatFontSize) || chatFontSize < 12 || chatFontSize > 24)
28
+ throw new Error("appearance.chatFontSize must be between 12 and 24");
29
+ const font = String(a.font ?? current.font ?? DEFAULT_APPEARANCE.font);
30
+ if (!TEXT_FONTS.includes(font))
31
+ throw new Error(`appearance.font must be one of ${TEXT_FONTS.join(", ")}`);
32
+ const mono = String(a.mono ?? current.mono ?? DEFAULT_APPEARANCE.mono);
33
+ if (!MONO_FONTS.includes(mono))
34
+ throw new Error(`appearance.mono must be one of ${MONO_FONTS.join(", ")}`);
35
+ const look = String(a.look ?? current.look ?? DEFAULT_APPEARANCE.look);
36
+ if (!/^[a-z][a-z0-9-]{0,30}$/.test(look))
37
+ throw new Error("appearance.look must be a short lower-case id");
38
+ const custom = { ...(current.custom ?? {}) };
39
+ if (a.custom !== undefined && typeof a.custom === "object" && a.custom) {
40
+ for (const [lookId, values] of Object.entries(a.custom)) {
41
+ if (!/^[a-z][a-z0-9-]{0,30}$/.test(lookId))
42
+ throw new Error("appearance.custom: a look id is a short lower-case id");
43
+ if (values === null) {
44
+ delete custom[lookId];
45
+ continue;
46
+ }
47
+ if (typeof values !== "object")
48
+ throw new Error("appearance.custom: each look takes an object of adjustments");
49
+ const clean = {};
50
+ for (const [key, value] of Object.entries(values)) {
51
+ const kind = ADJUSTABLE[key];
52
+ if (!kind)
53
+ throw new Error(`appearance.custom: "${key}" is not adjustable (${Object.keys(ADJUSTABLE).join(", ")})`);
54
+ if (kind === "colour") {
55
+ if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value))
56
+ throw new Error(`appearance.custom.${key} must be a colour like #1a2b3c`);
57
+ clean[key] = value.toLowerCase();
58
+ }
59
+ else {
60
+ const n = typeof value === "string" || typeof value === "number" ? Number(value) : NaN;
61
+ if (!Number.isFinite(n) || n < 0 || n > 2)
62
+ throw new Error(`appearance.custom.${key} must be a number between 0 and 2`);
63
+ clean[key] = String(Math.round(n * 100) / 100);
64
+ }
65
+ }
66
+ if (Object.keys(clean).length)
67
+ custom[lookId] = clean;
68
+ else
69
+ delete custom[lookId];
70
+ }
71
+ }
72
+ return { chatFontSize: Math.round(chatFontSize * 2) / 2, font, mono, look, custom };
73
+ }
24
74
  export class Hub extends EventEmitter {
25
75
  dataDir;
26
76
  rooms = new Map();
27
77
  skills;
28
78
  templates;
79
+ looks;
29
80
  settings;
30
81
  log;
31
82
  optionCache = new Map();
@@ -39,6 +90,7 @@ export class Hub extends EventEmitter {
39
90
  mkdirSync(join(this.dataDir, "rooms"), { recursive: true });
40
91
  this.skills = new SkillLibrary(join(this.dataDir, "skills"), log.child("skills"));
41
92
  this.templates = new TemplateLibrary(join(this.dataDir, "templates"), log.child("templates"));
93
+ this.looks = new LookLibrary(join(this.dataDir, "looks"), log.child("looks"));
42
94
  try {
43
95
  this.skills.seedBuiltins();
44
96
  }
@@ -51,6 +103,30 @@ export class Hub extends EventEmitter {
51
103
  hubUrl: () => this.hubUrl,
52
104
  templates: this.templates,
53
105
  templatesChanged: () => this.emit("event", { type: "templates" }),
106
+ looks: {
107
+ list: () => this.looks.list(),
108
+ describe: () => this.looks.describe(),
109
+ check: (raw) => checkLookSpec(raw),
110
+ save: (raw, options) => this.saveLook(raw, options),
111
+ },
112
+ appearance: {
113
+ current: () => ({ ...this.settings.appearance, custom: { ...(this.settings.appearance.custom ?? {}) } }),
114
+ preview: (patch) => nextAppearance(this.settings.appearance, patch),
115
+ apply: (patch) => {
116
+ this.updateSettings({ appearance: patch });
117
+ },
118
+ ownAdjustments: async (lookId) => {
119
+ const tokens = await loadTokens();
120
+ let look = tokens.looks[lookId];
121
+ if (!look) {
122
+ const spec = this.looks.get(lookId);
123
+ if (!spec)
124
+ return null;
125
+ look = tokens.make(spec);
126
+ }
127
+ return { label: look.label, values: Object.fromEntries(tokens.adjustables.map((f) => [f.key, String(f.of(look))])) };
128
+ },
129
+ },
54
130
  needApproval: () => this.settings.agentSkillsNeedApproval === true,
55
131
  save: (draft) => {
56
132
  const { body: _b, ...meta } = this.saveSkillInternal(draft);
@@ -209,54 +285,7 @@ export class Hub extends EventEmitter {
209
285
  next.editor = { mode, command };
210
286
  }
211
287
  if (patch.appearance !== undefined && typeof patch.appearance === "object" && patch.appearance) {
212
- const a = patch.appearance;
213
- const chatFontSize = Number(a.chatFontSize ?? next.appearance?.chatFontSize ?? DEFAULT_APPEARANCE.chatFontSize);
214
- if (!Number.isFinite(chatFontSize) || chatFontSize < 12 || chatFontSize > 24)
215
- throw new Error("appearance.chatFontSize must be between 12 and 24");
216
- const font = String(a.font ?? next.appearance?.font ?? DEFAULT_APPEARANCE.font);
217
- if (!TEXT_FONTS.includes(font))
218
- throw new Error(`appearance.font must be one of ${TEXT_FONTS.join(", ")}`);
219
- const mono = String(a.mono ?? next.appearance?.mono ?? DEFAULT_APPEARANCE.mono);
220
- if (!MONO_FONTS.includes(mono))
221
- throw new Error(`appearance.mono must be one of ${MONO_FONTS.join(", ")}`);
222
- const look = String(a.look ?? next.appearance?.look ?? DEFAULT_APPEARANCE.look);
223
- if (!/^[a-z][a-z0-9-]{0,30}$/.test(look))
224
- throw new Error("appearance.look must be a short lower-case id");
225
- const custom = { ...(next.appearance?.custom ?? {}) };
226
- if (a.custom !== undefined && typeof a.custom === "object" && a.custom) {
227
- for (const [lookId, values] of Object.entries(a.custom)) {
228
- if (!/^[a-z][a-z0-9-]{0,30}$/.test(lookId))
229
- throw new Error("appearance.custom: a look id is a short lower-case id");
230
- if (values === null) {
231
- delete custom[lookId];
232
- continue;
233
- }
234
- if (typeof values !== "object")
235
- throw new Error("appearance.custom: each look takes an object of adjustments");
236
- const clean = {};
237
- for (const [key, value] of Object.entries(values)) {
238
- const kind = ADJUSTABLE[key];
239
- if (!kind)
240
- throw new Error(`appearance.custom: "${key}" is not adjustable (${Object.keys(ADJUSTABLE).join(", ")})`);
241
- if (kind === "colour") {
242
- if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value))
243
- throw new Error(`appearance.custom.${key} must be a colour like #1a2b3c`);
244
- clean[key] = value.toLowerCase();
245
- }
246
- else {
247
- const n = typeof value === "string" || typeof value === "number" ? Number(value) : NaN;
248
- if (!Number.isFinite(n) || n < 0 || n > 2)
249
- throw new Error(`appearance.custom.${key} must be a number between 0 and 2`);
250
- clean[key] = String(Math.round(n * 100) / 100);
251
- }
252
- }
253
- if (Object.keys(clean).length)
254
- custom[lookId] = clean;
255
- else
256
- delete custom[lookId];
257
- }
258
- }
259
- next.appearance = { chatFontSize: Math.round(chatFontSize * 2) / 2, font, mono, look, custom };
288
+ next.appearance = nextAppearance(next.appearance, patch.appearance);
260
289
  }
261
290
  next.appearance = { ...DEFAULT_APPEARANCE, ...(next.appearance ?? {}) };
262
291
  if (!next.editor)
@@ -402,7 +431,7 @@ export class Hub extends EventEmitter {
402
431
  });
403
432
  if (!choice.agentType) {
404
433
  try {
405
- room.addUnstaffed({ name: choice.name || tv.name, tagline: tv.tagline, role: tv.role, avatar: tv.avatar, skills });
434
+ room.addUnstaffed({ name: choice.name || tv.name, tagline: tv.tagline, role: tv.role, avatar: tv.avatar, skills, textCheck: "notice" });
406
435
  }
407
436
  catch (error) {
408
437
  notices.push(`${choice.name || tv.name} could not be added: ${error instanceof Error ? error.message : String(error)}`);
@@ -421,12 +450,15 @@ export class Hub extends EventEmitter {
421
450
  model: choice.model ?? tv.model,
422
451
  effort: choice.effort ?? tv.effort,
423
452
  mode: choice.mode ?? tv.mode,
453
+ textCheck: "notice",
424
454
  });
425
455
  }
426
456
  catch (error) {
427
457
  notices.push(`${choice.name || tv.name} could not be summoned: ${error instanceof Error ? error.message : String(error)}`);
428
458
  }
429
459
  }
460
+ if (room.settings.customRules.length > room.settings.briefTextLimit)
461
+ notices.push(`The room rules from the template are ${room.settings.customRules.length} characters, over this room's limit of ${room.settings.briefTextLimit}; kept as they are, but the next edit has to fit.`);
430
462
  this.saveRooms();
431
463
  return { room, notices };
432
464
  }
@@ -492,12 +524,28 @@ export class Hub extends EventEmitter {
492
524
  this.update = update;
493
525
  this.emit("event", { type: "update", update });
494
526
  }
527
+ async saveLook(raw, options) {
528
+ const saved = await this.looks.save(raw, options);
529
+ this.emit("event", { type: "looks", looks: this.looks.list() });
530
+ return saved;
531
+ }
532
+ removeLook(id) {
533
+ const removed = this.looks.remove(id);
534
+ if (!removed)
535
+ return false;
536
+ if (this.settings.appearance?.look === id || this.settings.appearance?.custom?.[id]) {
537
+ this.updateSettings({ appearance: { look: this.settings.appearance.look === id ? DEFAULT_APPEARANCE.look : this.settings.appearance.look, custom: { [id]: null } } });
538
+ }
539
+ this.emit("event", { type: "looks", looks: this.looks.list() });
540
+ return true;
541
+ }
495
542
  snapshot() {
496
543
  return {
497
544
  settings: this.settings,
498
545
  update: this.update,
499
546
  recipes: listRecipes().map(({ build: _b, ...r }) => r),
500
547
  skills: this.skills.list(),
548
+ looks: this.looks.list(),
501
549
  roomDefaults: { ...DEFAULT_ROOM_SETTINGS, ...this.settings.roomDefaults },
502
550
  rooms: [...this.rooms.values()].map((room) => room.snapshot()),
503
551
  openRooms: [...this.openRooms],
@@ -518,6 +566,7 @@ export class Hub extends EventEmitter {
518
566
  rmSync(join(this.dataDir, "rooms"), { recursive: true, force: true });
519
567
  rmSync(this.roomsPath(), { force: true });
520
568
  rmSync(this.skills.dir, { recursive: true, force: true });
569
+ rmSync(this.looks.dir, { recursive: true, force: true });
521
570
  mkdirSync(join(this.dataDir, "rooms"), { recursive: true });
522
571
  mkdirSync(this.skills.dir, { recursive: true });
523
572
  this.skills.list();
package/dist/jsonrpc.js CHANGED
@@ -11,7 +11,9 @@ export class RemoteError extends Error {
11
11
  rpc;
12
12
  method;
13
13
  constructor(rpc, method) {
14
- super(`${method} failed: ${rpc.message} (code ${rpc.code})`);
14
+ const data = rpc.data;
15
+ const detail = typeof data === "string" ? data : data && typeof data === "object" && typeof data.details === "string" ? data.details : "";
16
+ super(`${method} failed: ${rpc.message}${detail ? `: ${detail}` : ""} (code ${rpc.code})`);
15
17
  this.rpc = rpc;
16
18
  this.method = method;
17
19
  }