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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibebuddy",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Your agent's busy. Come hang out. — tiny social hangout for vibe coders, MCP-native.",
5
5
  "type": "module",
6
6
  "bin": {
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('');
@@ -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
- // heartbeats are frequent throttle to one per 20s via an mtime file
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, '.hb');
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 {
@@ -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