ghostbus 0.0.0-stage → 0.5.2
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/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/PLAN.md +40 -0
- package/README.md +108 -2
- package/docs/client-setup.md +48 -0
- package/docs/hosted.md +126 -0
- package/examples/mcp_config.example.json +1 -0
- package/examples/three-agent-team.mjs +50 -0
- package/examples/two-agent-demo.mjs +34 -0
- package/examples/wake-hook/README.md +15 -0
- package/examples/wake-hook/wake.sh +39 -0
- package/package.json +46 -4
- package/src/board.html +78 -0
- package/src/cli.mjs +77 -0
- package/src/core.mjs +645 -0
- package/src/handler.mjs +166 -0
- package/src/hosted-server.mjs +239 -0
- package/src/http-server.mjs +47 -0
- package/src/mcp-server.mjs +61 -0
- package/src/tools.mjs +71 -0
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
|
+
"author": "Ghost Developer Studio",
|
|
3
|
+
"bin": {
|
|
4
|
+
"ghostbus": "src/mcp-server.mjs",
|
|
5
|
+
"ghostbus-cli": "src/cli.mjs",
|
|
6
|
+
"ghostbus-relay": "src/http-server.mjs"
|
|
7
|
+
},
|
|
8
|
+
"description": "Agent-to-agent message bus and shared workspace, exposed as an MCP server. Zero dependencies.",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"examples",
|
|
15
|
+
"docs",
|
|
16
|
+
"README.md",
|
|
17
|
+
"PLAN.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/littlestjames82-sys/ghostbus#readme",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"agent",
|
|
26
|
+
"multi-agent",
|
|
27
|
+
"message-bus",
|
|
28
|
+
"a2a",
|
|
29
|
+
"agent-to-agent"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
2
32
|
"name": "ghostbus",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/littlestjames82-sys/ghostbus.git"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"demo": "node examples/two-agent-demo.mjs",
|
|
39
|
+
"demo-team": "node examples/three-agent-team.mjs",
|
|
40
|
+
"relay": "node src/http-server.mjs",
|
|
41
|
+
"start": "node src/mcp-server.mjs",
|
|
42
|
+
"test": "node test.mjs",
|
|
43
|
+
"test:serverless": "node deploy/netlify/test-lib.mjs",
|
|
44
|
+
"hosted": "node src/hosted-server.mjs"
|
|
45
|
+
},
|
|
46
|
+
"type": "module",
|
|
47
|
+
"version": "0.5.2"
|
|
48
|
+
}
|
package/src/board.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>GhostBus — Shared Workspace Board</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --violet:#6716f3; --bg:#0e0c14; --panel:#17131f; --line:#2a2138; --text:#ece8f5; --muted:#9b8fb5; }
|
|
9
|
+
* { box-sizing:border-box; } body { margin:0; font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:var(--bg); color:var(--text); }
|
|
10
|
+
header { display:flex; align-items:center; gap:12px; padding:14px 20px; border-bottom:1px solid var(--line); }
|
|
11
|
+
header h1 { font-size:18px; margin:0; } header .dot { width:10px;height:10px;border-radius:50%;background:#3ddc84; }
|
|
12
|
+
header span { color:var(--muted); font-size:13px; }
|
|
13
|
+
main { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:16px; padding:16px 20px; }
|
|
14
|
+
section { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }
|
|
15
|
+
section h2 { font-size:13px; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); margin:0 0 10px; }
|
|
16
|
+
.agent, .task, .file, .ev { padding:7px 0; border-bottom:1px dashed var(--line); font-size:14px; }
|
|
17
|
+
.agent:last-child,.task:last-child,.file:last-child,.ev:last-child{border-bottom:none}
|
|
18
|
+
.pill { display:inline-block; padding:1px 8px; border-radius:99px; font-size:11px; background:#241a3d; color:#c9b8ff; margin-left:6px; }
|
|
19
|
+
.pill.green{ background:#123524; color:#7fe6ae; } .pill.amber{ background:#3a2c10; color:#ffd97a; }
|
|
20
|
+
.muted{ color:var(--muted); font-size:12px; } a{ color:#c9b8ff; }
|
|
21
|
+
#keybar { padding:10px 20px; border-bottom:1px solid var(--line); display:none; gap:8px; }
|
|
22
|
+
input { background:#0e0c14; border:1px solid var(--line); color:var(--text); border-radius:8px; padding:6px 10px; }
|
|
23
|
+
button { background:var(--violet); color:#fff; border:none; border-radius:8px; padding:6px 14px; cursor:pointer; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<header>
|
|
28
|
+
<div class="dot" id="dot"></div>
|
|
29
|
+
<h1>👻 GhostBus Board</h1>
|
|
30
|
+
<span id="ws">connecting…</span>
|
|
31
|
+
<span style="margin-left:auto" id="clock"></span>
|
|
32
|
+
</header>
|
|
33
|
+
<div id="keybar">
|
|
34
|
+
<span class="muted">This relay requires the workspace key:</span>
|
|
35
|
+
<input id="key" type="password" placeholder="GHOSTBUS_KEY" size="28">
|
|
36
|
+
<button onclick="saveKey()">Connect</button>
|
|
37
|
+
</div>
|
|
38
|
+
<main>
|
|
39
|
+
<section><h2>Agents</h2><div id="agents">—</div></section>
|
|
40
|
+
<section><h2>Open tasks</h2><div id="tasks">—</div></section>
|
|
41
|
+
<section><h2>Shared files</h2><div id="files">—</div></section>
|
|
42
|
+
<section><h2>Recent activity</h2><div id="events">—</div></section>
|
|
43
|
+
</main>
|
|
44
|
+
<script>
|
|
45
|
+
const $ = id => document.getElementById(id);
|
|
46
|
+
const BASE = location.pathname.replace(/\/$/, ''); // '' at root, '/w/<id>' on hosted relays
|
|
47
|
+
const key = () => localStorage.getItem('ghostbus_key') || '';
|
|
48
|
+
function saveKey(){ localStorage.setItem('ghostbus_key', $('key').value.trim()); $('keybar').style.display='none'; refresh(); }
|
|
49
|
+
async function api(path){
|
|
50
|
+
const r = await fetch(BASE + path, { headers: key() ? { 'x-bus-key': key() } : {} });
|
|
51
|
+
if (r.status === 401) { $('keybar').style.display = 'flex'; throw new Error('key needed'); }
|
|
52
|
+
if (!r.ok) throw new Error(path + ' ' + r.status);
|
|
53
|
+
return r.json();
|
|
54
|
+
}
|
|
55
|
+
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
56
|
+
async function refresh(){
|
|
57
|
+
try {
|
|
58
|
+
const [status, agents, tasks, files, events] = await Promise.all([
|
|
59
|
+
api('/api/status'), api('/api/agents'), api('/api/tasks'), api('/api/files/list'), api('/api/events?limit=12')
|
|
60
|
+
]);
|
|
61
|
+
$('ws').textContent = `${status.workspace.name} · v${status.version} · ${status.messages} messages`;
|
|
62
|
+
$('dot').style.background = '#3ddc84';
|
|
63
|
+
$('agents').innerHTML = agents.agents.length ? agents.agents.map(a =>
|
|
64
|
+
`<div class="agent"><strong>${esc(a.name)}</strong><span class="pill ${a.online?'green':''}">${a.online?'online':'away'}</span><br><span class="muted">${esc(a.role||'agent')}${a.capabilities?.length ? ' · ' + esc(a.capabilities.join(', ')) : ''} · last seen ${esc(a.lastSeenAt)}</span></div>`).join('') : '<span class="muted">none registered</span>';
|
|
65
|
+
const open = tasks.tasks.filter(t => !['done','cancelled'].includes(t.status));
|
|
66
|
+
$('tasks').innerHTML = open.length ? open.map(t =>
|
|
67
|
+
`<div class="task">#${t.id} <strong>${esc(t.title)}</strong><span class="pill ${t.status==='needs-approval'?'amber':''}">${esc(t.status)}</span><br><span class="muted">from ${esc(t.from)}${t.claimedBy ? ' · claimed by ' + esc(t.claimedBy) : ''}${t.assignee ? ' · for ' + esc(t.assignee) : ''}</span></div>`).join('') : '<span class="muted">queue is empty</span>';
|
|
68
|
+
$('files').innerHTML = files.files.length ? files.files.map(f =>
|
|
69
|
+
`<div class="file">📄 ${esc(f.path)}<br><span class="muted">${f.bytes} bytes · ${esc(f.updatedBy)} · ${esc(f.updatedAt)}</span></div>`).join('') : '<span class="muted">no shared files</span>';
|
|
70
|
+
$('events').innerHTML = events.events.slice().reverse().map(e =>
|
|
71
|
+
`<div class="ev"><span class="muted">${esc(e.at)}</span> <strong>${esc(e.actor)}</strong> ${esc(e.summary)}</div>`).join('');
|
|
72
|
+
$('clock').textContent = 'updated ' + new Date().toLocaleTimeString();
|
|
73
|
+
} catch (e) { $('dot').style.background = '#e05252'; $('ws').textContent = 'disconnected: ' + e.message; }
|
|
74
|
+
}
|
|
75
|
+
refresh(); setInterval(refresh, 5000);
|
|
76
|
+
</script>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GhostBus terminal CLI — drive a workspace from the shell (handy for humans,
|
|
4
|
+
* scripts, and wake hooks). Works directly on a store file:
|
|
5
|
+
*
|
|
6
|
+
* ghostbus-cli --store ./ghostbus-data.json status
|
|
7
|
+
* ghostbus-cli register alice --role planner
|
|
8
|
+
* ghostbus-cli send alice bob "ship it" (to = * broadcasts)
|
|
9
|
+
* ghostbus-cli inbox bob
|
|
10
|
+
* ghostbus-cli task alice "Build the API" [--assign bob] [--urgent] [--approve-first]
|
|
11
|
+
* ghostbus-cli tasks [--status queued]
|
|
12
|
+
* ghostbus-cli claim 1 bob | complete 1 bob "done, tests green" | comment 1 alice "note"
|
|
13
|
+
* ghostbus-cli board | agents | search "API" | put-file alice path.md ./local.md | get-file path.md
|
|
14
|
+
* ghostbus-cli export ./backup.json (full-state snapshot to a file, or stdout with -)
|
|
15
|
+
* ghostbus-cli import ./backup.json (restore a snapshot INTO this store — replaces state)
|
|
16
|
+
*
|
|
17
|
+
* Env: GHOSTBUS_STORE, GHOSTBUS_TOKEN_<AGENT> for token-enforced buses.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import { createBus, MemoryStore, FileStore, GHOSTBUS_VERSION } from './core.mjs';
|
|
21
|
+
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
const storeIdx = argv.indexOf('--store');
|
|
24
|
+
const storePath = storeIdx >= 0 ? argv[storeIdx + 1] : (process.env.GHOSTBUS_STORE || './ghostbus-data.json');
|
|
25
|
+
const args = argv.filter((a, i) => i !== storeIdx && i !== storeIdx + 1);
|
|
26
|
+
const [cmd, ...rest] = args;
|
|
27
|
+
const flag = (name) => rest.includes(name);
|
|
28
|
+
const flagVal = (name) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : null; };
|
|
29
|
+
const positional = rest.filter((a, i) => !a.startsWith('--') && rest[i - 1] !== '--assign' && rest[i - 1] !== '--status' && rest[i - 1] !== '--role');
|
|
30
|
+
const tokenFor = (agent) => process.env[`GHOSTBUS_TOKEN_${String(agent).toUpperCase().replace(/[^A-Z0-9]/g, '_')}`] || null;
|
|
31
|
+
|
|
32
|
+
const store = storePath === ':memory:' ? new MemoryStore() : new FileStore(storePath);
|
|
33
|
+
const bus = await createBus(store, {}, { requireTokens: process.env.GHOSTBUS_REQUIRE_TOKENS === '1' });
|
|
34
|
+
const out = (v) => console.log(typeof v === 'string' ? v : JSON.stringify(v, null, 2));
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
switch (cmd) {
|
|
38
|
+
case 'status': out(await bus.status()); break;
|
|
39
|
+
case 'agents': out(await bus.listAgents()); break;
|
|
40
|
+
case 'register': {
|
|
41
|
+
const r = await bus.registerAgent({ name: positional[0], role: flagVal('--role') || '', token: tokenFor(positional[0]) });
|
|
42
|
+
out(r); if (r.token) console.error('⚠ agent token shown ONCE — store it (CLI: GHOSTBUS_TOKEN_' + positional[0].toUpperCase() + ')');
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
case 'send': out(await bus.sendMessage({ from: positional[0], to: positional[1], body: positional.slice(2).join(' '), token: tokenFor(positional[0]) })); break;
|
|
46
|
+
case 'inbox': out(await bus.inbox(positional[0], { unreadOnly: flag('--unread'), token: tokenFor(positional[0]) })); break;
|
|
47
|
+
case 'task': out(await bus.createTask({ from: positional[0], title: positional.slice(1).join(' '), assignee: flagVal('--assign'), priority: flag('--urgent') ? 'urgent' : 'normal', needsApproval: flag('--approve-first'), token: tokenFor(positional[0]) })); break;
|
|
48
|
+
case 'tasks': out(await bus.listTasks({ status: flagVal('--status') })); break;
|
|
49
|
+
case 'claim': out(await bus.claimTask(Number(positional[0]), { by: positional[1], token: tokenFor(positional[1]) })); break;
|
|
50
|
+
case 'approve': out(await bus.approveTask(Number(positional[0]), { by: positional[1], token: tokenFor(positional[1]) })); break;
|
|
51
|
+
case 'complete': out(await bus.completeTask(Number(positional[0]), { by: positional[1], result: positional.slice(2).join(' '), token: tokenFor(positional[1]) })); break;
|
|
52
|
+
case 'comment': out(await bus.addTaskComment(Number(positional[0]), { by: positional[1], body: positional.slice(2).join(' '), token: tokenFor(positional[1]) })); break;
|
|
53
|
+
case 'board': out(await bus.board()); break;
|
|
54
|
+
case 'search': out(await bus.search(positional.join(' '))); break;
|
|
55
|
+
case 'put-file': out(await bus.putFile({ by: positional[0], path: positional[1], text: fs.readFileSync(positional[2], 'utf8'), token: tokenFor(positional[0]) })); break;
|
|
56
|
+
case 'get-file': { const f = await bus.getFile(positional[0]); out(f.text); break; }
|
|
57
|
+
case 'events': out(await bus.events({ limit: Number(flagVal('--limit') || 20) })); break;
|
|
58
|
+
case 'export': {
|
|
59
|
+
const env = { ghostbusExport: 1, version: GHOSTBUS_VERSION, exportedAt: new Date().toISOString(), workspace: (await bus.status()).workspace, state: await bus.snapshot() };
|
|
60
|
+
const text = JSON.stringify(env, null, 2);
|
|
61
|
+
if (!positional[0] || positional[0] === '-') console.log(text);
|
|
62
|
+
else { fs.writeFileSync(positional[0], text); console.error(`exported ${env.state.agents.length} agents, ${env.state.tasks.length} tasks, ${env.state.messages.length} messages -> ${positional[0]}`); }
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case 'import': {
|
|
66
|
+
if (!positional[0]) { console.error('usage: ghostbus-cli import <backup.json>'); process.exit(2); }
|
|
67
|
+
const parsed = JSON.parse(fs.readFileSync(positional[0], 'utf8'));
|
|
68
|
+
const state = parsed.ghostbusExport ? parsed.state : (parsed.state || parsed);
|
|
69
|
+
const st = await bus.restore(state, { by: 'cli' });
|
|
70
|
+
out({ imported: true, agents: st.agents, messages: st.messages, tasks: st.tasks.total });
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
console.error('usage: ghostbus-cli [--store file] <status|agents|register|send|inbox|task|tasks|claim|approve|complete|comment|board|search|put-file|get-file|events|export|import>');
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
} catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
|