viberoom 0.3.1 → 0.4.1

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.
Files changed (65) hide show
  1. package/NOTICE +12 -0
  2. package/README.md +123 -96
  3. package/dist/fsbrowse.js +65 -0
  4. package/dist/hub.js +18 -0
  5. package/dist/launcher.js +22 -0
  6. package/dist/main.js +4 -3
  7. package/dist/persona.js +1 -1
  8. package/dist/recipes.js +37 -14
  9. package/dist/room.js +3 -2
  10. package/dist/server.js +30 -0
  11. package/package.json +8 -5
  12. package/scripts/vendor-acp.mjs +85 -0
  13. package/ui/app.css +53 -1
  14. package/ui/app.js +360 -8
  15. package/ui/index.html +28 -1
  16. package/vendor/acp/claude-agent-acp/LICENSE +191 -0
  17. package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
  18. package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
  19. package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
  20. package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
  21. package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
  22. package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
  23. package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
  24. package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
  25. package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
  26. package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
  27. package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
  28. package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
  29. package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
  30. package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
  31. package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
  32. package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
  33. package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
  34. package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
  35. package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
  36. package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
  37. package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
  38. package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
  39. package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
  40. package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
  41. package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
  42. package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
  43. package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
  44. package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
  45. package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
  46. package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
  47. package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
  48. package/vendor/acp/claude-agent-acp/package.json +7 -0
  49. package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
  50. package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
  51. package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
  52. package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
  53. package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
  54. package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
  55. package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
  56. package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
  57. package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
  58. package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
  59. package/vendor/acp/claude-agent-sdk/package.json +7 -0
  60. package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
  61. package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
  62. package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
  63. package/vendor/acp/codex-acp/LICENSE +190 -0
  64. package/vendor/acp/codex-acp/dist/index.js +34238 -0
  65. package/vendor/acp/codex-acp/package.json +7 -0
package/NOTICE CHANGED
@@ -11,6 +11,18 @@ This product bundles the following third-party material:
11
11
  registry (https://agentclientprotocol.com). Each mark belongs to its owner and
12
12
  is used only to identify the agent it stands for.
13
13
 
14
+ - ACP adapters (vendor/acp/, copied from the npm packages at build time by
15
+ scripts/vendor-acp.mjs, each with its licence file):
16
+ - @agentclientprotocol/claude-agent-acp: Apache License 2.0, Copyright (c)
17
+ the Agent Client Protocol contributors.
18
+ - @agentclientprotocol/codex-acp: Apache License 2.0, Copyright (c) the Agent
19
+ Client Protocol contributors.
20
+ - @anthropic-ai/claude-agent-sdk (the library the Claude adapter is built on,
21
+ unmodified): (c) Anthropic PBC, all rights reserved; use is subject to
22
+ Anthropic's legal agreements (https://code.claude.com/docs/en/legal-and-compliance).
23
+ The agents themselves (Claude Code, Codex CLI) are not part of viberoom: it
24
+ runs the copies the user installed.
25
+
14
26
  - Mermaid (served from node_modules at run time, not copied): MIT License,
15
27
  Copyright (c) 2014-2025 Knut Sveidqvist.
16
28
 
package/README.md CHANGED
@@ -5,29 +5,106 @@
5
5
  <h1 align="center">viberoom</h1>
6
6
 
7
7
  <p align="center"><strong>One chat, many coding agents.</strong><br>
8
- Open a room, summon Claude, Codex, Gemini, Cursor, OpenCode or Copilot into it, and let them talk to you and to each other.</p>
8
+ Open a room, summon the agents you already have, give each one a role, and let them work it out with you and with each other.</p>
9
9
 
10
10
  <p align="center">
11
11
  <a href="https://www.npmjs.com/package/viberoom"><img src="https://img.shields.io/npm/v/viberoom?color=6c63ff&label=npm" alt="npm"></a>
12
12
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A5%2022-6c63ff" alt="Node 22+"></a>
13
- <a href="https://agentclientprotocol.com"><img src="https://img.shields.io/badge/protocol-ACP-6c63ff" alt="Agent Client Protocol"></a>
14
13
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-6c63ff" alt="AGPL-3.0"></a>
15
14
  </p>
16
15
 
17
16
  <p align="center">
18
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="900" alt="Three vibemates working through a task together">
17
+ <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/conversation.png" width="960" alt="Three vibemates arguing out a dedupe rule in a room">
19
18
  </p>
20
19
 
21
20
  <p align="center">
22
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>
23
- <sub>every agent you already have, each with its own login, in one room</sub>
22
+ <sub>whichever of them you have installed, with your own logins, in one room</sub>
24
23
  </p>
25
24
 
26
- ## Why
25
+ <br>
27
26
 
28
- A single agent is a conversation. Several agents in one room are a **team**: one defines what
29
- "done" means, one builds, one reviews, and you steer. viberoom gives that team a place to meet, a
30
- shared folder to work in, and rules for who speaks when.
27
+ ## The idea
28
+
29
+ One agent is a conversation. Three agents in one room are a **team**: one decides what "done" means,
30
+ one builds it, one says no. You set the roles and the rules, then talk to the room the way you would
31
+ talk to people. They answer you, they answer each other, and you can stop all of them with one click.
32
+
33
+ <p align="center">
34
+ <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/art/room.svg" width="960" alt="What a room is: a folder and a topic, vibemates with a character, rules and skills">
35
+ </p>
36
+
37
+ <br>
38
+
39
+ ## Give them roles
40
+
41
+ A vibemate is any agent on your machine plus a character: a **vibename**, a **vibeface**, a one-line
42
+ **vibersona** everyone sees, and a private **vibio** only this vibemate reads. Pick the model, the
43
+ effort and how much it may do on its own. Three vibemates from the same agent with three different
44
+ vibios behave like three different people, which is exactly the point.
45
+
46
+ <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">
48
+ </p>
49
+
50
+ <br>
51
+
52
+ ## Set the room rules
53
+
54
+ Rules are plain sentences. Who talks to whom, how long a reply may be, what happens on step three.
55
+ Type `@` and the name becomes a live reference: rename the vibemate and the rule follows. Every
56
+ vibemate gets the rules as instructions, so the room runs the way you wrote it down.
57
+
58
+ <p align="center">
59
+ <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
+ </p>
61
+
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.
64
+
65
+ <p align="center">
66
+ <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">
67
+ </p>
68
+
69
+ <br>
70
+
71
+ ## Teach them skills
72
+
73
+ A skill is a reusable instruction with a name. Keep them in one library, attach them to the vibemates
74
+ that need them, and run one with `/name` in the chat. A vibemate can write a skill too, and the
75
+ library marks it as theirs until you have read it.
76
+
77
+ <p align="center">
78
+ <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/skills.png" width="820" alt="The skills library: a skill written by a vibemate, and a built-in one">
79
+ </p>
80
+
81
+ <br>
82
+
83
+ ## Talk to all, or to one
84
+
85
+ <p align="center">
86
+ <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/art/ways-to-talk.svg" width="960" alt="Four ways to speak: to everyone, to one with @, run a skill with /, and Hush">
87
+ </p>
88
+
89
+ Write to the room and every vibemate answers, each after a short pause so replies do not trip over
90
+ each other. `@Name` one of them and the rest read along. Vibemates address each other the same way,
91
+ and a hop limit keeps a two-agent argument from running all night. **Hush** stops every running reply
92
+ at once; the room stays quiet until you write again.
93
+
94
+ <br>
95
+
96
+ ## What you get back
97
+
98
+ Replies come as proper text: lists, tables, code. A file path in a reply opens the file; a `.md` or
99
+ `.csv` opens right in the room, rendered or as a table. A path with a line number opens your editor
100
+ at that line. A diagram block becomes a diagram. Change one of your own messages after the fact and
101
+ the vibemates are told, or the conversation rewinds to that point.
102
+
103
+ <p align="center">
104
+ <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">
105
+ </p>
106
+
107
+ <br>
31
108
 
32
109
  ## Sixty seconds to a room
33
110
 
@@ -41,76 +118,35 @@ viberoom
41
118
  3. **Summon vibemates.** Pick an agent, name it, give it a character. Repeat.
42
119
  4. **Say hello.** Everyone answers in turn. `@Name` one of them, `/name` runs a skill.
43
120
 
44
- ## Highlights
45
-
46
- - **Rooms.** A room is a folder and a topic. Every vibemate works in that folder, and the history
47
- stays with the room. Move the folder and the vibemates move with it.
48
- - **Vibemates.** Summon any installed agent, give it a name, a one-line persona, a private brief and
49
- a face. Pick the model, the effort and the permission mode per vibemate.
50
- - **Talk to all, or to one.** Write to the room and everyone answers in turn; `@Name` one of them and
51
- the others listen. Vibemates read each other's replies and address each other the same way.
52
- - **Turn taking.** Every addressed vibemate answers at once by default, each after a short random
53
- delay so replies do not cross; or one at a time, the others waiting their turn. A hop limit keeps
54
- agent-to-agent chatter from running away.
55
- - **Hush.** One click stops every running reply; the vibemates stay quiet until you write again.
56
- - **Skills.** Reusable instructions in a library. Attach them to vibemates, invoke one with `/name`,
57
- or let a vibemate write its own.
58
- - **Markdown, links, files and diagrams.** Replies render as Markdown: lists, tables, code. Links and
59
- file paths open on your machine; a `.md` or `.csv` path opens right in the room (rendered, or as a
60
- table); a path with a `:line` opens in your editor at that line; a ```` ```mermaid ```` block
61
- becomes a diagram.
62
- - **Edit a message.** Change what you said: the vibemates are told, or the conversation is rewound.
63
- - **A desktop app.** A hidden hub, an app window of its own, a Start Menu / Dock / desktop icon.
64
-
65
- ## A tour
66
-
67
- <table>
68
- <tr>
69
- <td width="50%" valign="top">
70
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/welcome.png" alt="Welcome: set your vibe"><br>
71
- <sub><strong>Your vibe.</strong> The first start asks how the vibemates should know you: a name, a face, a line about you.</sub>
72
- </td>
73
- <td width="50%" valign="top">
74
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/home.png" alt="Home page"><br>
75
- <sub><strong>Home.</strong> Open a room, come back to the last ones, and see what a room can do.</sub>
76
- </td>
77
- </tr>
78
- <tr>
79
- <td width="50%" valign="top">
80
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/summon.png" alt="Summon a vibemate"><br>
81
- <sub><strong>Summon a vibemate.</strong> Pick the agent, the model, the effort and the permission mode; name it and give it a face.</sub>
82
- </td>
83
- <td width="50%" valign="top">
84
- <img src="https://raw.githubusercontent.com/todor-rusev/viberoom/main/docs/screenshots/vibemate.png" alt="A vibemate's persona"><br>
85
- <sub><strong>Its character.</strong> A public one-liner under the name, and a private brief only this vibemate reads.</sub>
86
- </td>
87
- </tr>
88
- </table>
89
-
90
- ## Requirements
121
+ The menu that appears can put an icon on your desktop, open viberoom in a window of its own, or open
122
+ it in a browser tab. Later, `npm install -g viberoom@latest` and the next start picks up the new version.
91
123
 
92
- - Node.js 22 or newer.
93
- - At least one coding agent installed on this machine. viberoom bundles the ACP adapters for Claude
94
- Code and Codex and finds Gemini CLI, Cursor, OpenCode and Copilot when they are installed. Each
95
- agent uses its own login and subscription; viberoom never sees your keys.
96
- - A Chromium-based browser (Chrome, Edge, Brave) for the app window; any modern browser for a tab.
124
+ <br>
97
125
 
98
- ## Install
126
+ ## Small things you will like
99
127
 
100
- `npm install -g viberoom`, then `viberoom`. The menu that appears can install a desktop icon, open the app window, or open a browser tab
101
- (Start Menu on Windows, `~/Applications/viberoom.app` on macOS, an applications-menu entry on Linux).
102
- Upgrade with `npm install -g viberoom@latest`; the next start replaces the running hub.
128
+ - **Rooms remember.** The history stays with the room. Move the folder and the vibemates move with it.
129
+ - **The window remembers.** Where it was, how big, which screen. Unplug that screen and it walks back.
130
+ - **Nothing leaves your machine** except what each agent sends to its own provider. viberoom never
131
+ sees your keys; every agent keeps its own login.
132
+ - **Edit a message.** Fix what you said; the vibemates get the memo, or the conversation rewinds.
133
+ - **Your messages on a timeline.** A thin strip on the chat's right edge, one mark per message of yours:
134
+ hover for the message with its neighbours, click to jump there.
135
+ - **Pick a folder from a tree.** Browse the machine's folders when a room needs one; make a new one on the spot.
136
+ - **Settings save themselves.** Change a setting and it is saved: on Enter, on leaving the field, on a pick.
137
+ - **Search the room.** Everything anyone said, one search box.
138
+ - **For geeks.** Every panel folds its technical settings behind a toggle. You never have to open it.
103
139
 
104
- From a clone instead:
140
+ <br>
105
141
 
106
- ```sh
107
- git clone https://github.com/todor-rusev/viberoom.git
108
- cd viberoom
109
- npm run install:global
110
- ```
142
+ ## What you need
111
143
 
112
- `install:global` builds the project, links the `viberoom` command and creates the launcher; later,
113
- `npm run update` rebuilds and replaces the running hub.
144
+ - Node.js 22 or newer.
145
+ - At least one coding agent installed and logged in: Claude Code, Codex, Gemini CLI, Cursor, OpenCode
146
+ or GitHub Copilot. viberoom finds the ones you have and offers only those. It installs none of them.
147
+ - A browser. Chrome, Edge or Brave for the app window; anything modern for a tab.
148
+
149
+ <br>
114
150
 
115
151
  ## Use
116
152
 
@@ -124,38 +160,29 @@ viberoom logs the background hub's log
124
160
  ```
125
161
 
126
162
  `Enter` sends, `Shift+Enter` is a new line, `@Name` addresses one vibemate, `/name` invokes a skill.
163
+ Your data lives in `~/.viberoom` (or `$VIBEROOM_DATA_DIR`): settings, rooms with their history,
164
+ skills and the log.
127
165
 
128
- Data lives in `~/.viberoom` (or `$VIBEROOM_DATA_DIR`): settings, rooms with their history, skills,
129
- transcripts and the hub log. Nothing leaves your machine except what each agent sends to its own
130
- provider.
131
-
132
- ## Settings worth knowing
133
-
134
- - **Vibemates act without asking.** On by default: agents edit files and run commands in the room's
135
- folder without a permission prompt. Turn it off to be asked, or change it per vibemate.
136
- - **Custom rules.** One rule per line in the room's panel, handed to every vibemate as instructions:
137
- who speaks to whom, how long a reply may be, where code goes.
138
- - **Language.** Vibemates follow the language you write in, or a fixed one for the room.
139
- - **Reply delay.** With two or more vibemates in a room, each waits a random few seconds before it
140
- answers. A vibemate can override it; alone it answers at once.
141
- - **Open files at a line.** Which editor gets `path:line` clicks: the first one found (VS Code,
142
- Cursor, Windsurf, Zed, Sublime Text, Notepad++, JetBrains) or your own command.
143
- - **Diagrams.** Colours for Mermaid diagrams: multicolour boxes by default, or a single tone.
144
- - **For geeks.** Every panel folds its technical settings behind a "for geeks" toggle.
166
+ <br>
145
167
 
146
168
  ## Development
147
169
 
148
170
  ```sh
171
+ git clone https://github.com/todor-rusev/viberoom.git
172
+ cd viberoom
149
173
  npm install
150
- npm run build # TypeScript to dist/
151
- node dist/main.js # run the hub in this terminal
174
+ npm run build # then: node dist/main.js
152
175
  ```
153
176
 
154
- The web UI in `ui/` is plain HTML, CSS and JavaScript served by the hub; reload the page to see a
155
- change. `scripts/render-icon.mjs` renders the app icon from `assets/icon-vector.svg`.
177
+ `npm run install:global` builds, links the `viberoom` command and creates the launcher; later,
178
+ `npm run update` rebuilds and replaces the running hub.
156
179
 
157
180
  ## License
158
181
 
159
- GNU Affero General Public License v3.0 or later. You may use, change and share viberoom; if you
160
- distribute a changed version, or run one as a service for others, share your changes under the same
161
- licence. See `LICENSE` and `NOTICE` (third-party fonts and icons).
182
+ AGPL-3.0-or-later. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
183
+
184
+ <br>
185
+
186
+ ---
187
+
188
+ <p align="center">🎬 &nbsp;Idea, screenplay and direction of viberoom: <strong>Silviya Ruzhina</strong></p>
@@ -0,0 +1,65 @@
1
+ // viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readdir } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
6
+ export function listRoots(platform = process.platform, exists = existsSync) {
7
+ if (platform !== "win32")
8
+ return [{ name: "/", path: "/", hidden: false }];
9
+ const roots = [];
10
+ for (let code = "C".charCodeAt(0); code <= "Z".charCodeAt(0); code++) {
11
+ const drive = `${String.fromCharCode(code)}:\\`;
12
+ if (exists(drive))
13
+ roots.push({ name: drive.slice(0, 2), path: drive, hidden: false });
14
+ }
15
+ return roots;
16
+ }
17
+ export function homeFolder() {
18
+ return homedir();
19
+ }
20
+ export function normalizeFolder(input) {
21
+ let trimmed = input.trim().replace(/^["']|["']$/g, "");
22
+ if (/^[A-Za-z]:$/.test(trimmed))
23
+ trimmed += "\\";
24
+ if (!trimmed || !isAbsolute(trimmed))
25
+ throw new Error("an absolute folder path is needed");
26
+ const full = resolve(trimmed);
27
+ return /^[A-Za-z]:\\?$/.test(full) ? `${full.slice(0, 2)}\\` : full.replace(new RegExp(`\\${sep}+$`), "") || sep;
28
+ }
29
+ function isRoot(path) {
30
+ return path === sep || /^[A-Za-z]:\\?$/.test(path);
31
+ }
32
+ export async function listFolders(input) {
33
+ const path = normalizeFolder(input);
34
+ const entries = await readdir(path, { withFileTypes: true });
35
+ const dirs = [];
36
+ for (const entry of entries) {
37
+ let isDir = entry.isDirectory();
38
+ if (!isDir && entry.isSymbolicLink()) {
39
+ try {
40
+ isDir = (await readdir(join(path, entry.name), { withFileTypes: true })) !== undefined;
41
+ }
42
+ catch {
43
+ isDir = false;
44
+ }
45
+ }
46
+ if (!isDir)
47
+ continue;
48
+ dirs.push({ name: entry.name, path: join(path, entry.name), hidden: entry.name.startsWith(".") || entry.name.startsWith("$") });
49
+ }
50
+ dirs.sort((a, b) => (a.hidden === b.hidden ? a.name.localeCompare(b.name, undefined, { sensitivity: "base" }) : a.hidden ? 1 : -1));
51
+ return { path, parent: isRoot(path) ? null : dirname(path), dirs };
52
+ }
53
+ export function validFolderName(name) {
54
+ const n = name.trim();
55
+ return n.length > 0 && n.length <= 120 && n !== "." && n !== ".." && !/[\\/:*?"<>|\u0000-\u001f]/.test(n) && !/[. ]$/.test(n);
56
+ }
57
+ export async function createFolder(parent, name) {
58
+ if (!validFolderName(name))
59
+ throw new Error("a folder name cannot contain \\ / : * ? \" < > | and cannot end with a dot or a space");
60
+ const path = join(normalizeFolder(parent), name.trim());
61
+ if (existsSync(path))
62
+ throw new Error(`"${basename(path)}" already exists here`);
63
+ await mkdir(path);
64
+ return path;
65
+ }
package/dist/hub.js CHANGED
@@ -122,6 +122,24 @@ export class Hub extends EventEmitter {
122
122
  return defaults;
123
123
  }
124
124
  }
125
+ saveWindowPlacement(report) {
126
+ const num = (v) => typeof v === "number" && Number.isFinite(v);
127
+ if (!num(report.left) || !num(report.top) || !num(report.width) || !num(report.height))
128
+ throw new Error("left, top, width and height must be numbers");
129
+ const screen = report.screen;
130
+ const record = {
131
+ left: Math.round(report.left),
132
+ top: Math.round(report.top),
133
+ width: Math.round(report.width),
134
+ height: Math.round(report.height),
135
+ maximized: !!report.maximized,
136
+ savedAt: new Date().toISOString(),
137
+ };
138
+ if (screen && num(screen.left) && num(screen.top) && num(screen.width) && num(screen.height)) {
139
+ record.screen = { left: Math.round(screen.left), top: Math.round(screen.top), width: Math.round(screen.width), height: Math.round(screen.height) };
140
+ }
141
+ writeJson(join(this.dataDir, "window.json"), record);
142
+ }
125
143
  updateSettings(patch) {
126
144
  const next = { ...this.settings, roomDefaults: { ...this.settings.roomDefaults }, vendorPresets: { ...this.settings.vendorPresets } };
127
145
  if (patch.humanName !== undefined) {
package/dist/launcher.js CHANGED
@@ -120,6 +120,28 @@ export function savedWindowPlacement(profileDir) {
120
120
  return null;
121
121
  }
122
122
  }
123
+ export function windowFilePath(dataDir) {
124
+ return join(dataDir, "window.json");
125
+ }
126
+ export function recordedWindowPlacement(dataDir) {
127
+ try {
128
+ const o = JSON.parse(readFileSync(windowFilePath(dataDir), "utf8"));
129
+ const num = (v) => typeof v === "number" && Number.isFinite(v);
130
+ if (!num(o.left) || !num(o.top) || !num(o.width) || !num(o.height))
131
+ return null;
132
+ if (o.width < 200 || o.height < 150)
133
+ return null;
134
+ const placement = { left: o.left, top: o.top, width: o.width, height: o.height, maximized: !!o.maximized };
135
+ const s = o.screen;
136
+ if (s && num(s.left) && num(s.top) && num(s.width) && num(s.height) && s.width >= 200 && s.height >= 150) {
137
+ placement.workArea = { left: s.left, top: s.top, right: s.left + s.width, bottom: s.top + s.height };
138
+ }
139
+ return placement;
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
123
145
  export function windowFlags(placement) {
124
146
  const wa = placement.workArea;
125
147
  if (placement.maximized && wa && wa.right - wa.left >= 200 && wa.bottom - wa.top >= 150) {
package/dist/main.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { Hub } from "./hub.js";
9
9
  import { Logger } from "./log.js";
10
10
  import { startServer } from "./server.js";
11
- import { appWindowArgs, savedWindowPlacement, findChromium, isProcessAlive, logFilePath, openUrlCommand, pidFilePath, readPidFile, rotateLog, splitCommand, tailFile, writePidFile, } from "./launcher.js";
11
+ import { appWindowArgs, recordedWindowPlacement, savedWindowPlacement, findChromium, isProcessAlive, logFilePath, openUrlCommand, pidFilePath, readPidFile, rotateLog, splitCommand, tailFile, writePidFile, } from "./launcher.js";
12
12
  import { aumidSyncScript, installShortcuts, windowsShortcutPaths } from "./shortcuts.js";
13
13
  import { runMenu } from "./tui.js";
14
14
  function parseArgs(argv) {
@@ -177,10 +177,11 @@ function openWindow(url, options, log) {
177
177
  const fresh = !existsSync(profile);
178
178
  mkdirSync(profile, { recursive: true });
179
179
  reapHiddenBrowser(profile, log);
180
- const placement = fresh ? null : savedWindowPlacement(profile);
180
+ const recorded = fresh ? null : recordedWindowPlacement(options.dataDir);
181
+ const placement = recorded ?? (fresh ? null : savedWindowPlacement(profile));
181
182
  const args = appWindowArgs(url, profile, fresh, placement);
182
183
  const where = placement
183
- ? `last seen at ${placement.left},${placement.top} ${placement.width}x${placement.height}${placement.maximized ? " maximized" : ""}${placement.workArea ? ` on the screen ${placement.workArea.left},${placement.workArea.top}-${placement.workArea.right},${placement.workArea.bottom}` : ""}; flags ${args.filter((a) => a.startsWith("--window-")).join(" ")}`
184
+ ? `last seen (${recorded ? "window's own report" : "browser profile"}) at ${placement.left},${placement.top} ${placement.width}x${placement.height}${placement.maximized ? " maximized" : ""}${placement.workArea ? ` on the screen ${placement.workArea.left},${placement.workArea.top}-${placement.workArea.right},${placement.workArea.bottom}` : ""}; flags ${args.filter((a) => a.startsWith("--window-")).join(" ")}`
184
185
  : "no saved placement";
185
186
  log.info(`opening the app window with ${chromium}: ${where}`);
186
187
  try {
package/dist/persona.js CHANGED
@@ -113,7 +113,7 @@ export function buildBrief(settings, persona, roster, previousNotes, skills) {
113
113
  .filter((l) => l.length > 0);
114
114
  if (custom.length) {
115
115
  lines.push("");
116
- lines.push(`Custom rules of this room (set by ${human}):`);
116
+ lines.push(`Room rules (set by ${human}):`);
117
117
  for (const rule of custom)
118
118
  lines.push(`- ${rule}`);
119
119
  }
package/dist/recipes.js CHANGED
@@ -4,6 +4,7 @@ import { existsSync, readdirSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { createRequire } from "node:module";
6
6
  import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
7
8
  const isWindows = process.platform === "win32";
8
9
  function resolvePackageEntry(packageName, relativeEntry) {
9
10
  try {
@@ -102,8 +103,30 @@ function resolveCopilot() {
102
103
  }
103
104
  return null;
104
105
  }
105
- const claudeEntry = resolvePackageEntry("@agentclientprotocol/claude-agent-acp", join("dist", "index.js"));
106
- const codexEntry = resolvePackageEntry("@agentclientprotocol/codex-acp", join("dist", "index.js"));
106
+ function resolveGlobalNpmBin(name) {
107
+ const root = resolveGlobalNpmRoot();
108
+ if (!root)
109
+ return null;
110
+ const candidates = isWindows ? [join(root, "..", `${name}.cmd`)] : [join(root, "..", "..", "bin", name)];
111
+ return candidates.find((c) => existsSync(c)) ?? null;
112
+ }
113
+ function resolveClaudeCode() {
114
+ const onPath = resolveOnPath(["claude"]);
115
+ if (onPath && !/\.(cmd|bat)$/i.test(onPath))
116
+ return onPath;
117
+ const native = join(homedir(), ".local", "bin", isWindows ? "claude.exe" : "claude");
118
+ if (existsSync(native))
119
+ return native;
120
+ return resolveGlobalPackageEntry("@anthropic-ai/claude-code", "cli.js");
121
+ }
122
+ function resolveCodex() {
123
+ return resolveGlobalNpmBin("codex") ?? resolveOnPath(["codex"]);
124
+ }
125
+ const vendorDir = join(dirname(fileURLToPath(import.meta.url)), "..", "vendor", "acp");
126
+ const claudeAdapter = join(vendorDir, "claude-agent-acp", "dist", "index.js");
127
+ const codexAdapter = join(vendorDir, "codex-acp", "dist", "index.js");
128
+ const claudeExe = resolveClaudeCode();
129
+ const codexExe = resolveCodex();
107
130
  const geminiEntry = resolvePackageEntry("@google/gemini-cli", join("bundle", "gemini.js")) ??
108
131
  resolveGlobalPackageEntry("@google/gemini-cli", join("bundle", "gemini.js"));
109
132
  const cursorAgent = resolveCursorAgent();
@@ -116,7 +139,7 @@ const recipes = [
116
139
  vendor: "Claude",
117
140
  icon: "/vendor-icons/claude.svg",
118
141
  tested: true,
119
- note: "Adapter around the Claude Agent SDK; uses the machine's Claude Code login and settings.",
142
+ note: "Adapter around the Claude Agent SDK, driving the Claude Code installed on this machine with its login and settings.",
120
143
  modelPresets: ["haiku", "sonnet", "opus", "default"],
121
144
  defaultModel: "sonnet",
122
145
  effortPresets: ["low", "medium", "high", "default"],
@@ -124,13 +147,13 @@ const recipes = [
124
147
  modePresets: ["default", "acceptEdits", "plan", "auto", "bypassPermissions"],
125
148
  defaultMode: "default",
126
149
  bypassMode: "bypassPermissions",
127
- unavailableReason: claudeEntry ? null : "@agentclientprotocol/claude-agent-acp is not installed",
128
- installedAt: claudeEntry,
129
- installHint: "npm install (bundled with viberoom); needs a Claude Code login on this machine",
150
+ unavailableReason: claudeExe ? null : "Claude Code not found",
151
+ installedAt: claudeExe,
152
+ installHint: "install Claude Code (npm install -g @anthropic-ai/claude-code, or the native installer) and log in with `claude`",
130
153
  build: ({ model }) => ({
131
154
  command: process.execPath,
132
- args: [claudeEntry ?? ""],
133
- env: model && model !== "default" ? { ANTHROPIC_MODEL: model } : undefined,
155
+ args: [claudeAdapter],
156
+ env: { CLAUDE_CODE_EXECUTABLE: claudeExe ?? "", ...(model && model !== "default" ? { ANTHROPIC_MODEL: model } : {}) },
134
157
  }),
135
158
  },
136
159
  {
@@ -139,7 +162,7 @@ const recipes = [
139
162
  vendor: "Codex",
140
163
  icon: "/vendor-icons/codex.svg",
141
164
  tested: true,
142
- note: "Adapter around the Codex App Server (bundled Codex CLI); uses the machine's Codex login (~/.codex) or CODEX_API_KEY. Mode 'agent' edits the working directory without asking; 'read-only' for a chat-only participant.",
165
+ note: "Adapter around the Codex App Server of the Codex CLI installed on this machine; uses its login (~/.codex) or CODEX_API_KEY. Mode 'agent' edits the working directory without asking; 'read-only' for a chat-only participant.",
143
166
  modelPresets: ["gpt-5.4-mini", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"],
144
167
  defaultModel: "gpt-5.4-mini",
145
168
  effortPresets: ["low", "medium", "high", "xhigh"],
@@ -147,13 +170,13 @@ const recipes = [
147
170
  modePresets: ["read-only", "agent", "agent-full-access"],
148
171
  defaultMode: "read-only",
149
172
  bypassMode: "agent-full-access",
150
- unavailableReason: codexEntry ? null : "@agentclientprotocol/codex-acp is not installed",
151
- installedAt: codexEntry,
152
- installHint: "npm install (bundled with viberoom); needs a Codex login (codex login) or CODEX_API_KEY",
173
+ unavailableReason: codexExe ? null : "Codex CLI not found",
174
+ installedAt: codexExe,
175
+ installHint: "install Codex (npm install -g @openai/codex) and log in with `codex login`",
153
176
  build: () => ({
154
177
  command: process.execPath,
155
- args: [codexEntry ?? ""],
156
- env: { NO_BROWSER: "1" },
178
+ args: [codexAdapter],
179
+ env: { CODEX_PATH: codexExe ?? "", NO_BROWSER: "1" },
157
180
  }),
158
181
  },
159
182
  {
package/dist/room.js CHANGED
@@ -556,8 +556,9 @@ export class Room extends EventEmitter {
556
556
  runtime.briefPending = `room rules: ${briefChanges.join(", ")}`;
557
557
  this.postSystem(`Room settings updated (${briefChanges.join(", ")}); agents get refreshed instructions on their next turn.`);
558
558
  }
559
- if (unknownRefs.length) {
560
- this.notice(`Custom rules mention ${unknownRefs.map((n) => `@${n}`).join(", ")}, who ${unknownRefs.length > 1 ? "are" : "is"} not in the room; left as plain text.`, "warn");
559
+ const missing = unknownRefs.filter((n) => n !== "Name");
560
+ if (missing.length) {
561
+ this.notice(`Room rules mention ${missing.map((n) => `@${n}`).join(", ")}, who ${missing.length > 1 ? "are" : "is"} not in the room; left as plain text.`, "warn");
561
562
  }
562
563
  return this.settings;
563
564
  }
package/dist/server.js CHANGED
@@ -8,6 +8,7 @@ import { dirname, join } from "node:path";
8
8
  import { existsSync as fileExists } from "node:fs";
9
9
  import { classifyOpenTarget, describeOpen, detectEditor, editorCommand, isExecutablePath, openCommand } from "./open.js";
10
10
  import { parseCsv, viewerKind, VIEWER_MAX_BYTES } from "./viewer.js";
11
+ import { createFolder, homeFolder, listFolders, listRoots } from "./fsbrowse.js";
11
12
  let editorFound;
12
13
  function currentEditor() {
13
14
  if (editorFound === undefined)
@@ -106,6 +107,26 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
106
107
  req.on("close", () => clients.delete(res));
107
108
  return;
108
109
  }
110
+ if (req.method === "GET" && path === "/api/fs/dirs") {
111
+ const at = url.searchParams.get("path");
112
+ if (!at) {
113
+ sendJson(res, 200, { ok: true, roots: listRoots(), home: homeFolder() });
114
+ return;
115
+ }
116
+ try {
117
+ sendJson(res, 200, { ok: true, ...(await listFolders(at)) });
118
+ }
119
+ catch (error) {
120
+ const code = error.code;
121
+ if (code === "ENOENT" || code === "ENOTDIR")
122
+ sendJson(res, 404, { error: `no such folder: ${at}` });
123
+ else if (code === "EACCES" || code === "EPERM")
124
+ sendJson(res, 403, { error: `no access to ${at}` });
125
+ else
126
+ throw error;
127
+ }
128
+ return;
129
+ }
109
130
  if (req.method === "GET" && path === "/api/file") {
110
131
  const target = classifyOpenTarget(url.searchParams.get("path") ?? "");
111
132
  if (!target || target.kind !== "path")
@@ -202,6 +223,15 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
202
223
  sendJson(res, 200, { ok: true, settings: hub.updateSettings(body) });
203
224
  return;
204
225
  }
226
+ if (path === "/api/fs/mkdir") {
227
+ sendJson(res, 200, { ok: true, path: await createFolder(String(body.parent ?? ""), String(body.name ?? "")) });
228
+ return;
229
+ }
230
+ if (path === "/api/window") {
231
+ hub.saveWindowPlacement(body);
232
+ sendJson(res, 200, { ok: true });
233
+ return;
234
+ }
205
235
  if (path === "/api/open") {
206
236
  const target = classifyOpenTarget(String(body.target ?? ""));
207
237
  if (!target)