vibebuddy 0.1.0 → 0.3.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/package.json +1 -1
- package/src/bin.js +52 -1
- package/src/hook-template.mjs +39 -10
- package/src/mcp-template.mjs +15 -0
package/package.json
CHANGED
package/src/bin.js
CHANGED
|
@@ -159,7 +159,7 @@ async function cmdInit(flags) {
|
|
|
159
159
|
(entry.hooks ?? []).some(
|
|
160
160
|
(h) => typeof h.command === 'string' && h.command.endsWith(`hook.mjs" ${ev}`)
|
|
161
161
|
);
|
|
162
|
-
for (const ev of ['SessionStart', 'PostToolUse', 'Stop', 'Notification']) {
|
|
162
|
+
for (const ev of ['SessionStart', 'PostToolUse', 'Stop', 'SessionEnd', 'Notification']) {
|
|
163
163
|
settings.hooks[ev] ??= [];
|
|
164
164
|
if (settings.hooks[ev].some((e) => isOurs(e, ev))) continue;
|
|
165
165
|
settings.hooks[ev].push({
|
|
@@ -223,6 +223,55 @@ async function cmdInit(flags) {
|
|
|
223
223
|
say();
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
async function cmdApp() {
|
|
227
|
+
if (process.platform !== 'win32') {
|
|
228
|
+
say('the desktop bubble is Windows-first — macOS/Linux builds are coming.');
|
|
229
|
+
say(`meanwhile: ${mint('https://vibebuddy.io')} works everywhere.`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
say();
|
|
233
|
+
say(bold(' vibebuddy app — installing the desktop bubble'));
|
|
234
|
+
const rel = await fetchJson('https://api.github.com/repos/daobahan/vibebuddy-app/releases/latest', {
|
|
235
|
+
headers: { 'User-Agent': 'vibebuddy-cli' },
|
|
236
|
+
}).catch(() => null);
|
|
237
|
+
const asset = rel?.data?.assets?.find((a) => a.name.endsWith('-setup.exe'));
|
|
238
|
+
if (!asset) {
|
|
239
|
+
say(' no Windows build published yet — check https://github.com/daobahan/vibebuddy-app/releases');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
say(dim(` downloading ${asset.name} (${Math.round(asset.size / 1024 / 1024)} MB)…`));
|
|
243
|
+
const home = vbHome();
|
|
244
|
+
fs.mkdirSync(home, { recursive: true });
|
|
245
|
+
const setupPath = path.join(home, 'app-setup.exe');
|
|
246
|
+
const resp = await fetch(asset.browser_download_url, { headers: { 'User-Agent': 'vibebuddy-cli' } });
|
|
247
|
+
if (!resp.ok) {
|
|
248
|
+
say(' download failed — try again in a minute.');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
fs.writeFileSync(setupPath, Buffer.from(await resp.arrayBuffer()));
|
|
252
|
+
say(dim(' installing silently…'));
|
|
253
|
+
const inst = spawnSync(setupPath, ['/S'], { stdio: 'ignore', timeout: 180_000 });
|
|
254
|
+
if (inst.status !== 0 && inst.status !== null) {
|
|
255
|
+
say(` installer exited with ${inst.status} — run it manually: ${setupPath}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
// find the installed exe (per-user NSIS install)
|
|
259
|
+
const candidates = [
|
|
260
|
+
path.join(process.env.LOCALAPPDATA ?? '', 'VibeBuddy', 'VibeBuddy.exe'),
|
|
261
|
+
path.join(process.env.LOCALAPPDATA ?? '', 'VibeBuddy', 'vibebuddy-app.exe'),
|
|
262
|
+
path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'VibeBuddy', 'VibeBuddy.exe'),
|
|
263
|
+
];
|
|
264
|
+
const exe = candidates.find((p) => fs.existsSync(p));
|
|
265
|
+
if (exe) {
|
|
266
|
+
const { spawn } = await import('node:child_process');
|
|
267
|
+
spawn(exe, [], { detached: true, stdio: 'ignore' }).unref();
|
|
268
|
+
say(` ${mint('✓')} installed and launched — look for the bubble in the corner of your screen`);
|
|
269
|
+
} else {
|
|
270
|
+
say(` ${mint('✓')} installed — launch "VibeBuddy" from the Start menu`);
|
|
271
|
+
}
|
|
272
|
+
say();
|
|
273
|
+
}
|
|
274
|
+
|
|
226
275
|
async function cmdStatus() {
|
|
227
276
|
const home = vbHome();
|
|
228
277
|
let cfg;
|
|
@@ -246,6 +295,7 @@ const flags = parseFlags(process.argv.slice(2));
|
|
|
246
295
|
const cmd = flags._[0];
|
|
247
296
|
|
|
248
297
|
if (cmd === 'init') await cmdInit(flags);
|
|
298
|
+
else if (cmd === 'app') await cmdApp();
|
|
249
299
|
else if (cmd === 'mcp') (await import('./mcp-template.mjs')).run();
|
|
250
300
|
else if (cmd === 'hook') {
|
|
251
301
|
await (await import('./hook-template.mjs')).run(flags._.slice(1));
|
|
@@ -255,6 +305,7 @@ else {
|
|
|
255
305
|
say('vibebuddy — your agent is busy. come hang out.');
|
|
256
306
|
say('');
|
|
257
307
|
say(' npx vibebuddy init connect this machine to vibebuddy');
|
|
308
|
+
say(' npx vibebuddy app install the desktop bubble (windows)');
|
|
258
309
|
say(' vibebuddy status print your digest');
|
|
259
310
|
say(' vibebuddy mcp run the MCP server (stdio)');
|
|
260
311
|
say('');
|
package/src/hook-template.mjs
CHANGED
|
@@ -5,6 +5,27 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
|
|
8
|
+
// read the hook payload from stdin (Claude Code pipes JSON), briefly and safely
|
|
9
|
+
function readStdin(timeoutMs = 300) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let data = '';
|
|
12
|
+
const done = () => resolve(data);
|
|
13
|
+
const t = setTimeout(done, timeoutMs);
|
|
14
|
+
try {
|
|
15
|
+
process.stdin.on('data', (c) => (data += c));
|
|
16
|
+
process.stdin.on('end', () => {
|
|
17
|
+
clearTimeout(t);
|
|
18
|
+
done();
|
|
19
|
+
});
|
|
20
|
+
process.stdin.on('error', () => {});
|
|
21
|
+
process.stdin.resume();
|
|
22
|
+
} catch {
|
|
23
|
+
clearTimeout(t);
|
|
24
|
+
done();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
export async function run(argv = process.argv.slice(2)) {
|
|
9
30
|
const home = process.env.VB_HOME || path.join(os.homedir(), '.vibebuddy');
|
|
10
31
|
let cfg;
|
|
@@ -19,15 +40,30 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
19
40
|
PostToolUse: 'heartbeat',
|
|
20
41
|
SubagentStop: 'heartbeat',
|
|
21
42
|
Stop: 'stop',
|
|
43
|
+
SessionEnd: 'session_end',
|
|
22
44
|
Notification: 'waiting_input',
|
|
23
45
|
'codex-notify': 'stop',
|
|
24
46
|
};
|
|
25
47
|
const type = MAP[argv[0]];
|
|
26
48
|
if (!type) return;
|
|
27
49
|
|
|
28
|
-
//
|
|
50
|
+
// each concurrent agent session gets its own presence lane
|
|
51
|
+
let sessionId = null;
|
|
52
|
+
if (argv[0] === 'codex-notify') {
|
|
53
|
+
try {
|
|
54
|
+
const j = JSON.parse(argv[1] ?? '{}');
|
|
55
|
+
sessionId = j['thread-id'] ?? j.thread_id ?? j['turn-id'] ?? null;
|
|
56
|
+
} catch {}
|
|
57
|
+
} else {
|
|
58
|
+
try {
|
|
59
|
+
const j = JSON.parse((await readStdin()) || '{}');
|
|
60
|
+
sessionId = j.session_id ?? j.sessionId ?? null;
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// heartbeats are frequent — throttle to one per 20s per session via an mtime file
|
|
29
65
|
if (type === 'heartbeat') {
|
|
30
|
-
const f = path.join(home, '.
|
|
66
|
+
const f = path.join(home, `.hb-${String(sessionId ?? 'x').replace(/[^\w-]/g, '').slice(0, 24)}`);
|
|
31
67
|
try {
|
|
32
68
|
if (Date.now() - fs.statSync(f).mtimeMs < 20_000) return;
|
|
33
69
|
} catch {}
|
|
@@ -36,13 +72,6 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
36
72
|
} catch {}
|
|
37
73
|
}
|
|
38
74
|
|
|
39
|
-
// drain stdin so the agent's pipe never blocks (Claude Code sends hook payload JSON)
|
|
40
|
-
try {
|
|
41
|
-
process.stdin.resume();
|
|
42
|
-
process.stdin.on('data', () => {});
|
|
43
|
-
process.stdin.on('error', () => {});
|
|
44
|
-
} catch {}
|
|
45
|
-
|
|
46
75
|
const ac = new AbortController();
|
|
47
76
|
const timer = setTimeout(() => ac.abort(), 3000);
|
|
48
77
|
timer.unref?.();
|
|
@@ -50,7 +79,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
50
79
|
await fetch(cfg.server + '/api/agent/event', {
|
|
51
80
|
method: 'POST',
|
|
52
81
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + cfg.token },
|
|
53
|
-
body: JSON.stringify({ type, agent_kind: cfg.agent_kind || 'claude-code' }),
|
|
82
|
+
body: JSON.stringify({ type, agent_kind: cfg.agent_kind || 'claude-code', session_id: sessionId }),
|
|
54
83
|
signal: ac.signal,
|
|
55
84
|
});
|
|
56
85
|
} catch {
|
package/src/mcp-template.mjs
CHANGED
|
@@ -67,6 +67,16 @@ const TOOLS = [
|
|
|
67
67
|
required: ['game_id', 'move'],
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'get_game',
|
|
72
|
+
description:
|
|
73
|
+
'Read the current board of a VibeBuddy game: cells array (0 empty, 1 player_a, 2 player_b), whose turn, status. gomoku: 15x15 row-major, 5 in a row wins. connect4: 7x6 row-major with gravity, 4 in a row wins.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: { game_id: { type: 'number' } },
|
|
77
|
+
required: ['game_id'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
70
80
|
];
|
|
71
81
|
|
|
72
82
|
function loadConfig() {
|
|
@@ -134,6 +144,11 @@ async function handleToolCall(cfg, name, args = {}) {
|
|
|
134
144
|
if (r.offline) return { error: 'vibebuddy unreachable — move not played' };
|
|
135
145
|
return r.data;
|
|
136
146
|
}
|
|
147
|
+
if (name === 'get_game') {
|
|
148
|
+
const r = await callServer(cfg, 'GET', `/api/games/${Number(args.game_id)}`);
|
|
149
|
+
if (r.offline) return { error: 'vibebuddy unreachable' };
|
|
150
|
+
return r.data;
|
|
151
|
+
}
|
|
137
152
|
return { error: `unknown tool: ${name}` };
|
|
138
153
|
}
|
|
139
154
|
|