vibebuddy 0.2.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibebuddy",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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', 'UserPromptSubmit', '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({
@@ -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