viberoom 0.5.6 → 0.5.8
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/README.md +1 -1
- package/dist/main.js +3 -2
- package/dist/recipes.js +19 -2
- package/dist/server.js +1 -1
- package/dist/shortcuts.js +10 -10
- package/dist/update.js +3 -3
- package/package.json +1 -1
- package/scripts/install.mjs +6 -1
- package/scripts/update.mjs +6 -1
- package/templates/maken-and-elaine/template.json +30 -0
- package/templates/wren-and-quinn/template.json +0 -30
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ 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
|
|
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">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
package/dist/main.js
CHANGED
|
@@ -192,7 +192,8 @@ function openWindow(url, options, log) {
|
|
|
192
192
|
}
|
|
193
193
|
catch {
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
mkdirSync(options.dataDir, { recursive: true });
|
|
196
|
+
spawn(chromium, args, { cwd: options.dataDir, detached: true, stdio: "ignore" }).unref();
|
|
196
197
|
if (process.platform === "win32") {
|
|
197
198
|
const shortcuts = windowsShortcutPaths(homedir(), process.env, true).filter((p) => existsSync(p));
|
|
198
199
|
if (shortcuts.length)
|
|
@@ -325,7 +326,7 @@ async function startBackground(options, log, info) {
|
|
|
325
326
|
const args = [fileURLToPath(import.meta.url), "serve", "--port", String(options.port), "--data-dir", options.dataDir, "--no-open"];
|
|
326
327
|
if (options.name)
|
|
327
328
|
args.push("--name", options.name);
|
|
328
|
-
const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", fd, fd], windowsHide: true });
|
|
329
|
+
const child = spawn(process.execPath, args, { cwd: options.dataDir, detached: true, stdio: ["ignore", fd, fd], windowsHide: true });
|
|
329
330
|
child.unref();
|
|
330
331
|
closeSync(fd);
|
|
331
332
|
log.info(`hub started in the background (pid ${child.pid}); log: ${logPath}`);
|
package/dist/recipes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } 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";
|
|
@@ -110,6 +110,23 @@ function resolveGlobalNpmBin(name) {
|
|
|
110
110
|
const candidates = isWindows ? [join(root, "..", `${name}.cmd`)] : [join(root, "..", "..", "bin", name)];
|
|
111
111
|
return candidates.find((c) => existsSync(c)) ?? null;
|
|
112
112
|
}
|
|
113
|
+
function resolveGlobalPackageBin(packageName, command) {
|
|
114
|
+
const root = resolveGlobalNpmRoot();
|
|
115
|
+
if (!root)
|
|
116
|
+
return null;
|
|
117
|
+
const dir = join(root, packageName);
|
|
118
|
+
try {
|
|
119
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
120
|
+
const entry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[command];
|
|
121
|
+
if (!entry)
|
|
122
|
+
return null;
|
|
123
|
+
const file = join(dir, entry);
|
|
124
|
+
return existsSync(file) ? file : null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
113
130
|
function resolveClaudeCode() {
|
|
114
131
|
const onPath = resolveOnPath(["claude"]);
|
|
115
132
|
if (onPath && !/\.(cmd|bat)$/i.test(onPath))
|
|
@@ -117,7 +134,7 @@ function resolveClaudeCode() {
|
|
|
117
134
|
const native = join(homedir(), ".local", "bin", isWindows ? "claude.exe" : "claude");
|
|
118
135
|
if (existsSync(native))
|
|
119
136
|
return native;
|
|
120
|
-
return
|
|
137
|
+
return resolveGlobalPackageBin("@anthropic-ai/claude-code", "claude");
|
|
121
138
|
}
|
|
122
139
|
function resolveCodex() {
|
|
123
140
|
return resolveGlobalNpmBin("codex") ?? resolveOnPath(["codex"]);
|
package/dist/server.js
CHANGED
|
@@ -193,7 +193,7 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
|
193
193
|
if (!latest)
|
|
194
194
|
throw new Error("no newer version is known; check for updates first");
|
|
195
195
|
log.info(`installing viberoom ${latest} (npm install -g)`);
|
|
196
|
-
const result = await installUpdate(latest);
|
|
196
|
+
const result = await installUpdate(latest, hub.dataDir);
|
|
197
197
|
if (!result.ok)
|
|
198
198
|
throw new Error(`npm install failed: ${result.output.slice(-600) || "no output"}`);
|
|
199
199
|
log.info(`viberoom ${latest} installed; starting the new build, which replaces this hub`);
|
package/dist/shortcuts.js
CHANGED
|
@@ -4,8 +4,8 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, writeFileSync } from "n
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
import { findChromium } from "./launcher.js";
|
|
7
|
-
export function vbsLauncher(node, main) {
|
|
8
|
-
return ["' viberoom: start the hub without a console window and open the app window.", 'Set sh = CreateObject("WScript.Shell")', `sh.Run """${node}"" ""${main}"" start", 0, False`, ""].join("\r\n");
|
|
7
|
+
export function vbsLauncher(node, main, workingDir) {
|
|
8
|
+
return ["' viberoom: start the hub without a console window and open the app window.", 'Set sh = CreateObject("WScript.Shell")', `sh.CurrentDirectory = "${workingDir}"`, `sh.Run """${node}"" ""${main}"" start", 0, False`, ""].join("\r\n");
|
|
9
9
|
}
|
|
10
10
|
const AUMID_BASE = { "chrome.exe": "Chrome", "msedge.exe": "MSEdge", "brave.exe": "Brave", "chromium.exe": "Chromium" };
|
|
11
11
|
export function appUserModelId(browserPath, url = "http://127.0.0.1:4810/", profileDirName = "browser") {
|
|
@@ -42,13 +42,13 @@ public static class LnkAumid {
|
|
|
42
42
|
}
|
|
43
43
|
}`;
|
|
44
44
|
const psq = (s) => s.replace(/'/g, "''");
|
|
45
|
-
export function shortcutScript(lnk, wscript, vbs,
|
|
45
|
+
export function shortcutScript(lnk, wscript, vbs, workingDir, ico, aumid = null) {
|
|
46
46
|
const lines = [
|
|
47
47
|
"$ErrorActionPreference = 'Stop'",
|
|
48
48
|
`$s = (New-Object -ComObject WScript.Shell).CreateShortcut('${psq(lnk)}')`,
|
|
49
49
|
`$s.TargetPath = '${psq(wscript)}'`,
|
|
50
50
|
`$s.Arguments = '"${psq(vbs)}"'`,
|
|
51
|
-
`$s.WorkingDirectory = '${psq(
|
|
51
|
+
`$s.WorkingDirectory = '${psq(workingDir)}'`,
|
|
52
52
|
`$s.Description = 'viberoom: rooms for you and your coding agents'`,
|
|
53
53
|
ico ? `$s.IconLocation = '${psq(ico)},0'` : "",
|
|
54
54
|
"$s.Save()",
|
|
@@ -82,8 +82,8 @@ export function windowsShortcutPaths(home, env, desktop) {
|
|
|
82
82
|
targets.push(join(home, "Desktop", "viberoom.lnk"));
|
|
83
83
|
return targets;
|
|
84
84
|
}
|
|
85
|
-
export function desktopEntry(node, main, icon) {
|
|
86
|
-
return ["[Desktop Entry]", "Type=Application", "Name=viberoom", "Comment=Rooms for you and your coding agents", `Exec="${node}" "${main}" start`, `Icon=${icon}`, "Terminal=false", "StartupWMClass=viberoom", "Categories=Development;Chat;", ""].join("\n");
|
|
85
|
+
export function desktopEntry(node, main, icon, workingDir) {
|
|
86
|
+
return ["[Desktop Entry]", "Type=Application", "Name=viberoom", "Comment=Rooms for you and your coding agents", `Exec="${node}" "${main}" start`, `Path=${workingDir}`, `Icon=${icon}`, "Terminal=false", "StartupWMClass=viberoom", "Categories=Development;Chat;", ""].join("\n");
|
|
87
87
|
}
|
|
88
88
|
export function macPlist(version) {
|
|
89
89
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -113,7 +113,7 @@ export function installShortcuts(o) {
|
|
|
113
113
|
const icnsSrc = join(o.root, "assets", "icon.icns");
|
|
114
114
|
if (platform === "win32") {
|
|
115
115
|
const vbs = join(launcherDir, "viberoom.vbs");
|
|
116
|
-
writeFileSync(vbs, vbsLauncher(o.node, main));
|
|
116
|
+
writeFileSync(vbs, vbsLauncher(o.node, main, o.dataDir));
|
|
117
117
|
result.files.push(vbs);
|
|
118
118
|
let ico = null;
|
|
119
119
|
if (existsSync(icoSrc)) {
|
|
@@ -126,7 +126,7 @@ export function installShortcuts(o) {
|
|
|
126
126
|
const aumid = appUserModelId(browser, "http://127.0.0.1:4810/", basename(join(o.dataDir, "browser")));
|
|
127
127
|
for (const lnk of windowsShortcutPaths(home, env, o.desktop)) {
|
|
128
128
|
mkdirSync(join(lnk, ".."), { recursive: true });
|
|
129
|
-
const r = spawnSync("powershell", ["-NoProfile", "-Command", shortcutScript(lnk, wscript, vbs, o.
|
|
129
|
+
const r = spawnSync("powershell", ["-NoProfile", "-Command", shortcutScript(lnk, wscript, vbs, o.dataDir, ico, aumid)], { encoding: "utf8" });
|
|
130
130
|
if (r.status === 0)
|
|
131
131
|
result.files.push(lnk);
|
|
132
132
|
else
|
|
@@ -142,7 +142,7 @@ export function installShortcuts(o) {
|
|
|
142
142
|
mkdirSync(join(app, "Contents", "Resources"), { recursive: true });
|
|
143
143
|
writeFileSync(join(app, "Contents", "Info.plist"), macPlist(o.version));
|
|
144
144
|
const exe = join(app, "Contents", "MacOS", "viberoom");
|
|
145
|
-
writeFileSync(exe, `#!/bin/sh\nexec "${o.node}" "${main}" start\n`);
|
|
145
|
+
writeFileSync(exe, `#!/bin/sh\ncd "${o.dataDir}" 2>/dev/null\nexec "${o.node}" "${main}" start\n`);
|
|
146
146
|
chmodSync(exe, 0o755);
|
|
147
147
|
if (existsSync(icnsSrc))
|
|
148
148
|
copyFileSync(icnsSrc, join(app, "Contents", "Resources", "icon.icns"));
|
|
@@ -157,7 +157,7 @@ export function installShortcuts(o) {
|
|
|
157
157
|
copyFileSync(pngSrc, icon);
|
|
158
158
|
result.files.push(icon);
|
|
159
159
|
}
|
|
160
|
-
const entry = desktopEntry(o.node, main, icon);
|
|
160
|
+
const entry = desktopEntry(o.node, main, icon, o.dataDir);
|
|
161
161
|
const appsDir = join(home, ".local", "share", "applications");
|
|
162
162
|
mkdirSync(appsDir, { recursive: true });
|
|
163
163
|
const menuEntry = join(appsDir, "viberoom.desktop");
|
package/dist/update.js
CHANGED
|
@@ -74,10 +74,10 @@ export function installCommandLine(version) {
|
|
|
74
74
|
throw new Error(`not a version: ${version}`);
|
|
75
75
|
return `npm install -g viberoom@${version} --no-audit --no-fund`;
|
|
76
76
|
}
|
|
77
|
-
export function installUpdate(version) {
|
|
77
|
+
export function installUpdate(version, cwd) {
|
|
78
78
|
const line = installCommandLine(version);
|
|
79
79
|
return new Promise((resolve) => {
|
|
80
|
-
const child = process.platform === "win32" ? spawn(line, { shell: true, windowsHide: true }) : spawn("npm", line.split(" ").slice(1));
|
|
80
|
+
const child = process.platform === "win32" ? spawn(line, { cwd, shell: true, windowsHide: true }) : spawn("npm", line.split(" ").slice(1), { cwd });
|
|
81
81
|
let output = "";
|
|
82
82
|
const collect = (chunk) => {
|
|
83
83
|
output = (output + chunk.toString()).slice(-4000);
|
|
@@ -90,6 +90,6 @@ export function installUpdate(version) {
|
|
|
90
90
|
}
|
|
91
91
|
export function restartWithNewBuild(mainModuleUrl, port, dataDir) {
|
|
92
92
|
const main = fileURLToPath(mainModuleUrl);
|
|
93
|
-
const child = spawn(process.execPath, [main, "start", "--port", String(port), "--data-dir", dataDir, "--no-open"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
93
|
+
const child = spawn(process.execPath, [main, "start", "--port", String(port), "--data-dir", dataDir, "--no-open"], { cwd: dataDir, detached: true, stdio: "ignore", windowsHide: true });
|
|
94
94
|
child.unref();
|
|
95
95
|
}
|
package/package.json
CHANGED
package/scripts/install.mjs
CHANGED
|
@@ -11,9 +11,14 @@ const args = new Set(process.argv.slice(2));
|
|
|
11
11
|
const dataDir = process.env.VIBEROOM_DATA_DIR ? resolve(process.env.VIBEROOM_DATA_DIR) : join(homedir(), ".viberoom");
|
|
12
12
|
const isWin = process.platform === "win32";
|
|
13
13
|
|
|
14
|
+
function npmEnv() {
|
|
15
|
+
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^npm_config_/i.test(name)));
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
function run(cmd, cmdArgs, opts = {}) {
|
|
15
19
|
console.log(`> ${cmd} ${cmdArgs.join(" ")}`);
|
|
16
|
-
const
|
|
20
|
+
const env = cmd === "npm" ? npmEnv() : process.env;
|
|
21
|
+
const r = isWin && cmd === "npm" ? spawnSync(`npm ${cmdArgs.join(" ")}`, { cwd: root, stdio: "inherit", env, shell: true, ...opts }) : spawnSync(cmd, cmdArgs, { cwd: root, stdio: "inherit", env, ...opts });
|
|
17
22
|
if (r.status !== 0) throw new Error(`${cmd} ${cmdArgs.join(" ")} failed with exit code ${r.status}`);
|
|
18
23
|
}
|
|
19
24
|
|
package/scripts/update.mjs
CHANGED
|
@@ -10,9 +10,14 @@ const args = new Set(process.argv.slice(2));
|
|
|
10
10
|
const isWin = process.platform === "win32";
|
|
11
11
|
const main = join(root, "dist", "main.js");
|
|
12
12
|
|
|
13
|
+
function npmEnv() {
|
|
14
|
+
return Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^npm_config_/i.test(name)));
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
function run(cmd, cmdArgs, opts = {}) {
|
|
14
18
|
console.log(`> ${cmd} ${cmdArgs.join(" ")}`);
|
|
15
|
-
const
|
|
19
|
+
const env = cmd === "npm" ? npmEnv() : process.env;
|
|
20
|
+
const r = isWin && cmd === "npm" ? spawnSync(`npm ${cmdArgs.join(" ")}`, { cwd: root, stdio: "inherit", env, shell: true, ...opts }) : spawnSync(cmd, cmdArgs, { cwd: root, stdio: "inherit", env, ...opts });
|
|
16
21
|
if (r.status !== 0) throw new Error(`${cmd} ${cmdArgs.join(" ")} failed with exit code ${r.status}`);
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Maken & Elaine",
|
|
3
|
+
"order": 1,
|
|
4
|
+
"emoji": "⚒️",
|
|
5
|
+
"recommended": true,
|
|
6
|
+
"description": "Two equals around one piece of work. Maken makes by default, Elaine explains by default; either does either when asked, and either reviews the other's work when you ask for it. The way viberoom itself was made.",
|
|
7
|
+
"settings": {
|
|
8
|
+
"language": {
|
|
9
|
+
"mode": "follow-human"
|
|
10
|
+
},
|
|
11
|
+
"turnTaking": "parallel",
|
|
12
|
+
"agentsWakeEachOther": true,
|
|
13
|
+
"hopLimit": 24,
|
|
14
|
+
"customRules": "Both make, both explain, both review; @ decides who does what. Without @, a task goes to the one who has worked on that part the most, or most recently; if neither has touched it, to Maken. A question without @ goes to Elaine.\nThe human decides when work is reviewed: ask the other one to review it. Unasked, the other answers questions and otherwise stays silent; it does not review, comment on or extend work it was not asked about. When asked to review, first say in one line what you will check, then check it by that criterion: a number, a live check, the risky case. Whoever made it reports to the human. Address the other vibemate too only when the report changes something it works on or relies on: a shared file, an interface, a convention, a measurement that overturns its claim; then say in one line what you want from it (\"nothing, for your context\" counts). Otherwise it reads the report later, unaddressed. A report is never a request for review; only the human asks for one. Finished work is reported as: what changed (the files, and the commit if the project uses version control), how to verify it (a command, a script, a screenshot), what was not verified, what is left.\nMeasure before diagnosing; say no with a reason and a cheaper alternative; keep replies short.\nWork only on your own task, never on the other's; do not touch what the other is working on.\nReply only when addressed. On a message to everyone, only the one it concerns answers; the other stays silent. Never add to, correct or comment on the other's reply unless the human asks.\nFollow the conventions of the project you work in (its instructions, notes, tests). Where it keeps decision notes, write the decision there before reporting it here.\nRead every proposal and analyse it; agree or reject only with a real argument. Never concede to be agreeable, never object to seem rigorous.\nReply in the human's language. Code identifiers, commands, file paths, protocol and tool names stay exactly as in the code; established technical terms (kernel, thread, commit, pull request, layout) stay in English."
|
|
15
|
+
},
|
|
16
|
+
"vibemates": [
|
|
17
|
+
{
|
|
18
|
+
"name": "Maken",
|
|
19
|
+
"tagline": "makes by default; explains and reviews when asked",
|
|
20
|
+
"role": "You are Maken, one of two equal vibemates, Maken and Elaine. You lean to making: a task without an address is yours when it touches what you have made, or when neither of you has; before you change anything, read what is already there, the notes, the docs and the files, and keep each change small. Everything else is in the room rules.",
|
|
21
|
+
"avatar": "🔨"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Elaine",
|
|
25
|
+
"tagline": "explains by default; makes and reviews when asked",
|
|
26
|
+
"role": "You are Elaine, one of two equal vibemates, Maken and Elaine. You lean to explaining: a question without an address is yours; when you review, it is by a criterion, not an impression. Everything else is in the room rules.",
|
|
27
|
+
"avatar": "💡"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Wren & Quinn",
|
|
3
|
-
"order": 1,
|
|
4
|
-
"emoji": "⚒️",
|
|
5
|
-
"recommended": true,
|
|
6
|
-
"description": "Two equals around one codebase. Wren builds by default, Quinn explains by default; either does either when asked, and either reviews the other's work when you ask for it. The way viberoom itself was built.",
|
|
7
|
-
"settings": {
|
|
8
|
-
"language": {
|
|
9
|
-
"mode": "follow-human"
|
|
10
|
-
},
|
|
11
|
-
"turnTaking": "parallel",
|
|
12
|
-
"agentsWakeEachOther": true,
|
|
13
|
-
"hopLimit": 24,
|
|
14
|
-
"customRules": "Both build, both explain, both review; @ decides who does what. Without @, a task goes to the one who has worked on that part the most, or most recently; if neither has touched it, to Wren. A question without @ goes to Quinn.\nThe human decides when work is reviewed: ask the other one to review it. Unasked, the other answers questions and otherwise stays silent; it does not review, comment on or extend work it was not asked about. When asked to review, first say in one line what you will check, then check it by that criterion: a number, a live check, the risky case. Whoever built reports to the human. Address the other vibemate too only when the report changes something it works on or relies on: a shared file, an interface, a convention, a measurement that overturns its claim; then say in one line what you want from it (\"nothing, for your context\" counts). Otherwise it reads the report later, unaddressed. A report is never a request for review; only the human asks for one. A build is reported as: what changed (the files, and the commit if the project uses version control), how to verify it (a command, a script, a screenshot), what was not verified, what is left.\nMeasure before diagnosing; say no with a reason and a cheaper alternative; keep replies short.\nWork only on your own task, never on the other's; do not touch what the other is working on.\nReply only when addressed. On a message to everyone, only the one it concerns answers; the other stays silent. Never add to, correct or comment on the other's reply unless the human asks.\nFollow the conventions of the project you work in (its instructions, notes, tests). Where it keeps decision notes, write the decision there before reporting it here.\nRead every proposal and analyse it; agree or reject only with a real argument. Never concede to be agreeable, never object to seem rigorous.\nReply in the human's language. Code identifiers, commands, file paths, protocol and tool names stay exactly as in the code; established technical terms (kernel, thread, commit, pull request, layout) stay in English."
|
|
15
|
-
},
|
|
16
|
-
"vibemates": [
|
|
17
|
-
{
|
|
18
|
-
"name": "Wren",
|
|
19
|
-
"tagline": "builds by default; explains and reviews when asked",
|
|
20
|
-
"role": "You are Wren, one of two equal vibemates, Wren and Quinn. You lean to building: a task without an address is yours when it touches what you have built, or when neither of you has; before you change anything, read what is already there, the notes, the docs and the code, and keep each change small. Everything else is in the room rules.",
|
|
21
|
-
"avatar": "🔨"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"name": "Quinn",
|
|
25
|
-
"tagline": "explains by default; builds and reviews when asked",
|
|
26
|
-
"role": "You are Quinn, one of two equal vibemates, Wren and Quinn. You lean to explaining: a question without an address is yours; when you review, it is by a criterion, not an impression. Everything else is in the room rules.",
|
|
27
|
-
"avatar": "💡"
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
}
|