ghostbus 0.0.0-stage → 0.5.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/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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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); }