Real-time awareness, coordination, and collaboration for Claude Code agents via Gigabuddy places. Agents see peers join and leave, what files they're focused…

Real-time awareness, coordination, and collaboration for Claude Code agents via Gigabuddy places. Agents see peers join and leave, what files they're focused on, conflicts, and messages — injected straight into context.
# In Claude Code:
/plugin marketplace add gigabuddy/agent-plugins
/plugin install gigabuddy
Restart Claude Code — plugin MCP servers only attach at session start.
The plugin is a thin shell: it launches the @gigabuddy/agent MCP server from npm, which connects your session to the Gigabuddy door. Sign in when prompted on first connect.
@gigabuddy/agent, the Gigabuddy agent client./decide, /handover, /pickup, /idea, /issue. Skill guidance is served live by the Gigabuddy door, so it improves without plugin updates.This repository contains the plugin shell only. The Gigabuddy platform is proprietary; see https://gigabuddy.com.
mod/register.tsx 108 lines1/**
2 * The Gigabuddy mod — Gigabuddy driving this Claude Code session from its live
3 * record (page:ihuoK970I4i8, decision:FWFiBJHe8lPw).
4 *
5 * The plugin's own MCP server (`agent`) already holds the live connection to
6 * Gigabuddy; this module reaches it with `$.mcp.call` and does one thing: a
7 * loop collects what the person sent from the session view
8 * (`session_commands_wait`, bounded, re-armed from a timer so no MCP call
9 * outlives its timeout and nothing polls a server). A message is submitted as
10 * the person's own words (`$.prompt.submit` with `asUser`, a turn of its own
11 * once the session is idle); a Stop ends the running turn.
12 *
13 * It does NOT decide tool prompts. A mod can only replace Claude Code's own
14 * permission dialog with a prompt of its own, which loses the dialog's choices
15 * (don't ask again, this session, the suggested rules, auto mode) — Daniel,
16 * 2026-10-09: not until a mod's prompt is at least as good. Answering the real
17 * dialog from Gigabuddy is the channel relay's job (libs/agent channel.ts),
18 * for a session launched with the channel: `claude
19 * --dangerously-load-development-channels plugin:gigabuddy@gigabuddy`. The
20 * server-side tools for a mod-driven approval (`permission_check` and
21 * friends) stay, unused, for when that prompt is built.
22 *
23 * A hook's budget counts its own time only, never a `$` call in flight, so the
24 * waits cost nothing. Nothing here runs under `-p` (no one to ask).
25 */
26import type { EngineInterface, Register } from 'claude-code';
27
28/** The server's key in this plugin's .mcp.json. */
29const SERVER_KEY = 'agent';
30/** One bounded wait on the server; the tools cap it there too. */
31const WAIT_MS = 25_000;
32/** Between two inbound waits — a breath, not a poll: the wait itself blocks until something arrives. */
33const REARM_MS = 50;
34
35/** The first text block of an MCP result, parsed as JSON; `{}` when it is not. */
36function parse(result: { content: Array<{ type: string; text?: string }> }): Record<string, unknown> {
37 const text = result.content.find((c) => c.type === 'text')?.text;
38 if (!text) return {};
39 try {
40 const parsed = JSON.parse(text) as unknown;
41 return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {};
42 } catch {
43 return {};
44 }
45}
46
47/** What the inbound loop needs of the module: the server's name, and the running turn for Stop. */
48type Link = { server: string | null; turnId: string | null };
49
50/** One wait on the server, then whatever came in, then the next wait. Re-armed from a timer: work that outlives a dispatch. */
51async function collect($: EngineInterface, link: Link): Promise<void> {
52 if (!link.server) return;
53 try {
54 const { commands } = parse(await $.mcp.call(link.server, 'session_commands_wait', { timeout_ms: WAIT_MS }));
55 for (const command of Array.isArray(commands) ? commands : [])
56 await handle($, link, command as Record<string, unknown>);
57 } catch (err) {
58 $.ui.log(`gigabuddy: session commands wait failed: ${String(err)}`, { to: 'debug' });
59 }
60 $.clock.after(REARM_MS, () => void collect($, link));
61}
62
63async function handle($: EngineInterface, link: Link, command: Record<string, unknown>): Promise<void> {
64 if (command['kind'] === 'message' && typeof command['text'] === 'string' && command['text']) {
65 // The person's own words, read bare; queued for a turn of its own once the session is idle.
66 await $.prompt.submit({ text: command['text'], asUser: true });
67 return;
68 }
69 if (command['kind'] === 'abort') {
70 if (!link.turnId) return;
71 try {
72 await $.turn.abort({ turnId: link.turnId });
73 $.ui.toast('Stopped from Gigabuddy');
74 } catch (err) {
75 $.ui.log(`gigabuddy: stop failed: ${String(err)}`, { to: 'debug' });
76 }
77 }
78}
79
80export const register: Register = (on) => {
81 /** The server's name as `$.mcp.call` takes it (null until connected), and the running turn. */
82 const link: Link = { server: null, turnId: null };
83
84 // ── Inbound: what the person sent from the session view ─────────────────────
85
86 on('session.start', async ($, e, next) => {
87 if (!e.isInteractive) return next(e);
88 const connected = await $.mcp.connect(SERVER_KEY);
89 if (connected.isConnected) {
90 link.server = connected.server;
91 $.clock.after(REARM_MS, () => void collect($, link));
92 } else {
93 $.ui.log(`gigabuddy: not driving this session from Gigabuddy (${connected.message})`, { to: 'debug' });
94 }
95 return next(e);
96 });
97
98 on('turn.start', ($, e, next) => {
99 link.turnId = e.turnId;
100 return next(e);
101 });
102
103 on('turn.complete', ($, e, next) => {
104 link.turnId = null;
105 return next(e);
106 });
107};
108