joinhive 2.0.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/bin/hive +820 -0
  4. package/bin/hive-claim-invite.mjs +88 -0
  5. package/bin/hive-join.mjs +243 -0
  6. package/bin/hive-keygen.mjs +48 -0
  7. package/bin/hive-mint.mjs +65 -0
  8. package/bin/hive-net.mjs +400 -0
  9. package/bin/hive-wallet.mjs +120 -0
  10. package/bin/hived.mjs +5 -0
  11. package/bin/setup-queen.sh +71 -0
  12. package/daemon/engines/anthropic.mjs +45 -0
  13. package/daemon/engines/cli.mjs +25 -0
  14. package/daemon/engines/index.mjs +84 -0
  15. package/daemon/engines/openai.mjs +42 -0
  16. package/daemon/fanout.mjs +47 -0
  17. package/daemon/hived.mjs +782 -0
  18. package/daemon/relay/client.mjs +196 -0
  19. package/daemon/relay/cursor.mjs +59 -0
  20. package/daemon/relay/ws.mjs +100 -0
  21. package/dev/compose.yml +109 -0
  22. package/docs/README.md +30 -0
  23. package/docs/SUMMARY.md +20 -0
  24. package/docs/a2a-events.md +82 -0
  25. package/docs/architecture.md +86 -0
  26. package/docs/cli.md +70 -0
  27. package/docs/concepts.md +50 -0
  28. package/docs/contracts.md +85 -0
  29. package/docs/http-api.md +78 -0
  30. package/docs/protocols.md +64 -0
  31. package/docs/quickstart.md +51 -0
  32. package/docs/security.md +53 -0
  33. package/docs/self-hosting.md +101 -0
  34. package/docs/tokenomics.md +63 -0
  35. package/install-remote.sh +49 -0
  36. package/join.sh +81 -0
  37. package/onchain/deploy-v2.sh +82 -0
  38. package/onchain/deployments.sepolia.json +14 -0
  39. package/onchain/foundry.toml +11 -0
  40. package/onchain/migrate-v2.mjs +76 -0
  41. package/onchain/src/Honey.sol +45 -0
  42. package/onchain/src/HoneyV2.sol +74 -0
  43. package/onchain/src/Jelly.sol +19 -0
  44. package/onchain/src/JellyV2.sol +31 -0
  45. package/package.json +72 -0
  46. package/protocols/book-recs.md +11 -0
  47. package/protocols/email-in-style.md +15 -0
  48. package/protocols/event-hunt.md +17 -0
  49. package/protocols/food-order.md +20 -0
  50. package/protocols/group-diagnosis.md +13 -0
  51. package/protocols/meta.md +11 -0
  52. package/protocols/movie-recs.md +17 -0
  53. package/protocols/predict.md +21 -0
  54. package/protocols/read-what-others-read.md +14 -0
  55. package/protocols/session-bounty.md +11 -0
  56. package/protocols/session-split-pool.md +10 -0
  57. package/server/Dockerfile +33 -0
  58. package/server/api.mjs +192 -0
  59. package/server/join-page.mjs +169 -0
  60. package/server/keygen-treasury.mjs +33 -0
  61. package/server/provision.mjs +262 -0
  62. package/server/rewarder.mjs +369 -0
  63. package/server/supervisor.mjs +237 -0
  64. package/server/treasury.mjs +172 -0
  65. package/shared/config-schema.mjs +94 -0
  66. package/shared/events.mjs +47 -0
  67. package/shared/nip-oa.mjs +56 -0
  68. package/shared/nip98.mjs +41 -0
  69. package/shared/redact.mjs +20 -0
  70. package/shared/rewards.json +33 -0
  71. package/shared/sealed.mjs +50 -0
  72. package/shared/txqueue.mjs +42 -0
  73. package/skills/hive-capability-store/SKILL.md +49 -0
  74. package/skills/hive-data-store/SKILL.md +60 -0
  75. package/skills/hive-join/SKILL.md +86 -0
  76. package/skills/hive-object-store/SKILL.md +45 -0
  77. package/skills/hive-prompt/SKILL.md +54 -0
  78. package/skills/hive-protocol-author/SKILL.md +92 -0
  79. package/skills/hive-wallet/SKILL.md +54 -0
  80. package/watcher/distill.mjs +248 -0
  81. package/watcher/global.nfh.hive.sync.plist.tmpl +20 -0
  82. package/watcher/sync.mjs +136 -0
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+ // hive-net — Node network helper so member laptops need NO Rust buzz binary.
3
+ // Speaks the same relay wire protocol as the daemon (shared client).
4
+ //
5
+ // hive-net ask "<text>" post an intent (typed + human-readable)
6
+ // hive-net list users|agents [--online]
7
+ // hive-net leaderboard HONEY ranks for every known wallet
8
+ // hive-net agent status|pause|resume your bee (control events are owner-signed)
9
+ // hive-net send <channel-name> <text> raw plaintext send
10
+ import { readFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join, dirname } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { RelayClient } from '../daemon/relay/client.mjs';
15
+ import { EV, tryJson } from '../shared/events.mjs';
16
+ import { redactSecrets } from '../shared/redact.mjs';
17
+ import { signedFetch } from '../shared/nip98.mjs';
18
+
19
+ const PACK_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
20
+ const HIVE_HOME = process.env.HIVE_HOME || join(homedir(), '.hive');
21
+ const loadJson = (p, fb) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return fb; } };
22
+ const cfg = loadJson(join(HIVE_HOME, 'config.json'), {});
23
+ const identity = loadJson(join(HIVE_HOME, 'identity.json'), null);
24
+ if (!identity) { console.error(JSON.stringify({ error: 'no identity; run: hive keygen (or hive join)' })); process.exit(1); }
25
+ const relay = new RelayClient({ relayUrl: process.env.BUZZ_RELAY_URL || cfg.relay || 'http://localhost:3000', privkey: identity.privkey, log: () => {} });
26
+ const chans = { lounge: 'hive-lounge', intents: 'hive-intents', logs: 'hive-logs', ...(cfg.channels || {}) };
27
+
28
+ const [cmd, ...rest] = process.argv.slice(2);
29
+
30
+ // Fold the recent bus into a member directory: pubkey -> {name, evm, is_bee, owner, last_at}.
31
+ const directory = async () => {
32
+ const logsId = await relay.ensureChannel(chans.logs);
33
+ const raw = await relay.query([{ kinds: [9, 40002], '#h': [logsId], limit: 1000 }]);
34
+ if (!raw) throw new Error('relay unreachable');
35
+ const dir = {};
36
+ const rows = raw.map(RelayClient.normalize).sort((a, b) => a.created_at - b.created_at);
37
+ for (const m of rows) {
38
+ const j = tryJson(m.content);
39
+ if (!j || (j.by && j.by !== m.pubkey)) continue; // R-B1
40
+ const e = dir[m.pubkey] || (dir[m.pubkey] = { pubkey: m.pubkey, last_at: 0 });
41
+ if (m.created_at > e.last_at) e.last_at = m.created_at;
42
+ if (j.type === EV.WALLET && /^0x[0-9a-fA-F]{40}$/.test(j.evm || '')) e.evm = j.evm;
43
+ if (j.type === EV.JOIN) { e.name = j.name; e.is_bee = !!j.is_bee; e.owner = j.owner_pubkey; e.owner_name = j.owner_name; }
44
+ }
45
+ return { dir, logsId };
46
+ };
47
+
48
+ const presence = async (pubkeys) => {
49
+ if (!pubkeys.length) return new Set();
50
+ const raw = await relay.query([{ kinds: [40902], authors: pubkeys, limit: pubkeys.length }]);
51
+ const online = new Set();
52
+ for (const e of raw || []) {
53
+ if (!/online|away/.test(e.content || '')) continue;
54
+ online.add(((e.tags || []).find((t) => t[0] === 'p') || [])[1] || e.pubkey);
55
+ }
56
+ return online;
57
+ };
58
+
59
+ const main = async () => {
60
+ if (cmd === 'ask') {
61
+ const text = rest.join(' ').trim();
62
+ if (!text) { console.error(JSON.stringify({ error: 'usage: hive ask "<what you want>"' })); process.exit(1); }
63
+ const [safe] = redactSecrets(text.slice(0, 300));
64
+ const logsId = await relay.ensureChannel(chans.logs);
65
+ const intentsId = await relay.ensureChannel(chans.intents);
66
+ const r = await relay.sendMessage(logsId, JSON.stringify({ type: EV.INTENT, intent: safe, origin: 'ask', for: identity.pubkey, by: identity.pubkey }));
67
+ await relay.sendMessage(intentsId, safe); // human-readable trace
68
+ console.log(JSON.stringify({ asked: safe, event: r.event_id, note: 'results land in: hive feed' }));
69
+ return;
70
+ }
71
+
72
+ if (cmd === 'list') {
73
+ const what = rest[0] === 'agents' ? 'agents' : 'users';
74
+ const onlineOnly = rest.includes('--online');
75
+ const { dir } = await directory();
76
+ const all = Object.values(dir);
77
+ const rows = what === 'agents' ? all.filter((e) => e.is_bee) : all.filter((e) => !e.is_bee);
78
+ // Resolve kind-0 profile names for anyone the bus directory can't name.
79
+ const unnamed = rows.filter((r) => !r.name).map((r) => r.pubkey);
80
+ if (unnamed.length) {
81
+ const profs = await relay.query([{ kinds: [0], authors: unnamed, limit: unnamed.length }]);
82
+ for (const p of profs || []) {
83
+ const meta = tryJson(p.content);
84
+ const row = rows.find((r) => r.pubkey === p.pubkey);
85
+ if (row && meta && (meta.display_name || meta.name)) row.name = meta.display_name || meta.name;
86
+ }
87
+ }
88
+ const online = await presence(rows.map((r) => r.pubkey));
89
+ const out = rows
90
+ .filter((r) => !onlineOnly || online.has(r.pubkey))
91
+ .sort((a, b) => b.last_at - a.last_at)
92
+ .map((r) => ({
93
+ [what === 'agents' ? 'agent' : 'user']: r.name || r.pubkey.slice(0, 12),
94
+ pubkey: r.pubkey.slice(0, 12),
95
+ ...(r.evm ? { wallet: `${r.evm.slice(0, 10)}…` } : {}),
96
+ ...(r.owner_name ? { owner: r.owner_name } : {}),
97
+ online: online.has(r.pubkey),
98
+ }));
99
+ if (!out.length) console.log(`(no ${what} seen on this community yet)`);
100
+ for (const r of out) console.log(`${r.online ? '🟢' : '⚪️'} ${r[what === 'agents' ? 'agent' : 'user']} ${r.pubkey}${r.wallet ? ` ${r.wallet}` : ''}${r.owner ? ` (owner: ${r.owner})` : ''}`);
101
+ return;
102
+ }
103
+
104
+ if (cmd === 'leaderboard') {
105
+ const dep = loadJson(join(PACK_DIR, 'onchain', 'deployments.sepolia.json'), {});
106
+ if (!dep.honey) { console.error(JSON.stringify({ error: 'HONEY not deployed' })); process.exit(1); }
107
+ const { dir } = await directory();
108
+ const withWallets = Object.values(dir).filter((e) => e.evm);
109
+ const { JsonRpcProvider, Contract, formatUnits } = await import('ethers');
110
+ const provider = new JsonRpcProvider(process.env.SEPOLIA_RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com');
111
+ const honey = new Contract(dep.honey, ['function balanceOf(address) view returns (uint256)'], provider);
112
+ // One shared wallet per member: dedupe by EVM address, preferring the
113
+ // bee-named row (the human and their bee are one economic identity).
114
+ const byEvm = {};
115
+ for (const e of withWallets) {
116
+ const cur = byEvm[e.evm];
117
+ if (!cur || (e.is_bee && !cur.is_bee)) byEvm[e.evm] = e;
118
+ }
119
+ const rows = await Promise.all(Object.values(byEvm).map(async (e) => {
120
+ let bal = 0n; try { bal = await honey.balanceOf(e.evm); } catch {}
121
+ return { name: e.name || e.pubkey.slice(0, 12), is_bee: !!e.is_bee, honey: Number(formatUnits(bal, 18)) };
122
+ }));
123
+ rows.sort((a, b) => b.honey - a.honey);
124
+ if (!rows.length) { console.log('(no wallets announced yet)'); return; }
125
+ console.log('🏆 HONEY leaderboard (reputation — soulbound, earned only)');
126
+ rows.forEach((r, i) => console.log(` ${String(i + 1).padStart(2)}. ${r.name}${r.is_bee ? ' 🐝' : ''} ${r.honey} HONEY`));
127
+ return;
128
+ }
129
+
130
+ if (cmd === 'agent') {
131
+ const sub = rest[0] || 'status';
132
+ // Owner-signed control events; the bee pubkey comes from config (written
133
+ // at join) or --bee.
134
+ const beePk = rest.includes('--bee') ? rest[rest.indexOf('--bee') + 1] : cfg.bee_pubkey;
135
+ if (sub === 'pause' || sub === 'resume') {
136
+ if (!/^[0-9a-f]{64}$/i.test(beePk || '')) { console.error(JSON.stringify({ error: 'no bee_pubkey in config — pass --bee <pubkey>' })); process.exit(1); }
137
+ const logsId = await relay.ensureChannel(chans.logs);
138
+ const r = await relay.sendMessage(logsId, JSON.stringify({ type: EV.CONTROL, action: sub, bee: beePk, by: identity.pubkey }));
139
+ console.log(JSON.stringify({ [sub]: beePk.slice(0, 12), event: r.event_id, note: 'takes effect within one poll tick' }));
140
+ return;
141
+ }
142
+ if (sub === 'status') {
143
+ if (cfg.server_url && cfg.bee_name) {
144
+ try {
145
+ const res = await fetch(`${cfg.server_url}/api/bees/${cfg.bee_name.replace(/\.bee$/, '')}/status`, { signal: AbortSignal.timeout(5000) });
146
+ console.log(JSON.stringify(await res.json(), null, 2));
147
+ return;
148
+ } catch { /* fall through to bus view */ }
149
+ }
150
+ const { dir } = await directory();
151
+ const mine = Object.values(dir).filter((e) => e.is_bee && e.owner === identity.pubkey);
152
+ console.log(JSON.stringify(mine.length ? mine : { note: 'no bee announced for your key yet' }, null, 2));
153
+ return;
154
+ }
155
+ console.error(JSON.stringify({ error: 'usage: hive agent status|pause|resume [--bee <pubkey>]' }));
156
+ process.exit(1);
157
+ }
158
+
159
+ if (cmd === 'feed') {
160
+ // Results / tips / gifts / settlements addressed to me — provenance-checked.
161
+ const logsId = await relay.ensureChannel(chans.logs);
162
+ const raw = await relay.query([{ kinds: [9, 40002], '#h': [logsId], limit: 600 }]);
163
+ if (!raw) throw new Error('relay unreachable');
164
+ const me = identity.pubkey;
165
+ const rows = raw.map(RelayClient.normalize).sort((a, b) => a.created_at - b.created_at);
166
+ const results = [], tips = [], gifts = [], settles = []; const myOpen = new Set();
167
+ for (const m of rows) {
168
+ const j = tryJson(m.content);
169
+ if (!j || (j.by && j.by !== m.pubkey)) continue;
170
+ if (j.type === EV.SESSION && m.pubkey === me && typeof j.session_id === 'string') myOpen.add(j.session_id);
171
+ }
172
+ for (const m of rows) {
173
+ const j = tryJson(m.content);
174
+ if (!j || (j.by && j.by !== m.pubkey)) continue;
175
+ if (j.type === EV.RESULT && j.for === me) results.push({ m, j });
176
+ else if (j.type === EV.TIP && j.to === me && Number(j.amount) > 0) tips.push({ m, j });
177
+ else if (j.type === EV.TRANSFER && j.to === me) gifts.push({ m, j });
178
+ else if (j.type === EV.SETTLE && myOpen.has(j.session_id)) settles.push({ m, j });
179
+ }
180
+ let any = false;
181
+ if (results.length) { any = true; console.log('RESULTS:'); for (const { m, j } of results.slice(-15))
182
+ console.log(` • ${j.intent || ''}\n ${String(j.result).replace(/\n/g, ' ').slice(0, 240)}\n [${(j.by || '').slice(0, 12)} · protocols: ${(j.protocols_used || []).join(',')}] react: hive react ${m.id} up`); }
183
+ if (tips.length) { any = true; console.log('TIPS RECEIVED:'); for (const { j } of tips.slice(-10))
184
+ console.log(` • +${j.amount} ${j.token || 'JELLY'} from ${(j.from || '').slice(0, 12)} tx: ${j.tx || '?'}`); }
185
+ if (gifts.length) { any = true; console.log('GIFTS RECEIVED:'); for (const { j } of gifts.slice(-10))
186
+ console.log(` • object ${String(j.object || j.name || '').slice(0, 16)} from ${(j.from || '').slice(0, 12)}`); }
187
+ if (settles.length) { any = true; console.log('SETTLEMENTS (sessions you opened):'); for (const { j } of settles.slice(-10))
188
+ console.log(` • [${j.status}] ${String(j.kind || '')}: ${String(j.result).replace(/\n/g, ' ').slice(0, 200)}`); }
189
+ if (!any) console.log('(feed empty — try: hive ask "<what you want>")');
190
+ return;
191
+ }
192
+
193
+ if (cmd === 'react') {
194
+ // react <result-event-id> up|down [note] — HUMAN feedback; this is what
195
+ // mints HONEY at the next epoch. The result's author is resolved from the
196
+ // SIGNER (never a self-asserted field).
197
+ const [rid, dir = 'up', ...noteParts] = rest;
198
+ if (!rid || !['up', 'down'].includes(dir)) { console.error(JSON.stringify({ error: 'usage: hive react <result-event-id> up|down [note]' })); process.exit(1); }
199
+ const hit = (await relay.query([{ ids: [rid], limit: 1 }]))?.[0];
200
+ if (!hit) { console.error(JSON.stringify({ error: 'result not found on the relay' })); process.exit(1); }
201
+ const j = tryJson(hit.content);
202
+ if (!j || (j.by && j.by !== hit.pubkey)) { console.error(JSON.stringify({ error: 'result unverifiable (provenance mismatch)' })); process.exit(1); }
203
+ const logsId = await relay.ensureChannel(chans.logs);
204
+ const note = noteParts.join(' ').trim();
205
+ const r = await relay.sendMessage(logsId, JSON.stringify({
206
+ type: EV.FEEDBACK, result: rid, result_by: hit.pubkey, dir,
207
+ ...(note ? { note: note.slice(0, 200) } : {}), by: identity.pubkey, at: Math.floor(Date.now() / 1000),
208
+ }));
209
+ console.log(JSON.stringify({ reacted: dir, result: rid.slice(0, 12), event: r.event_id }));
210
+ return;
211
+ }
212
+
213
+ if (cmd === 'admin-invite') {
214
+ // Operator-only: mints a member invite on the bee-host (which mints the
215
+ // relay invite via the steward) and prints the shareable join link.
216
+ // --uses N one code admitting N distinct members (default 2, max 100)
217
+ // --ttl-days D validity window (default 3, max 30)
218
+ const server = (rest.includes('--server') ? rest[rest.indexOf('--server') + 1] : cfg.server_url || '').replace(/\/+$/, '');
219
+ if (!server) { console.error(JSON.stringify({ error: 'no server_url in config — pass --server https://api.<domain>' })); process.exit(1); }
220
+ const uses = rest.includes('--uses') ? Number(rest[rest.indexOf('--uses') + 1]) : undefined;
221
+ const ttlDays = rest.includes('--ttl-days') ? Number(rest[rest.indexOf('--ttl-days') + 1]) : undefined;
222
+ const r = await signedFetch(identity.privkey, 'POST', `${server}/api/admin/invites`, {
223
+ ...(uses ? { max_uses: uses } : {}), ...(ttlDays ? { ttl_secs: Math.round(ttlDays * 86400) } : {}),
224
+ });
225
+ if (r.status !== 200) { console.error(JSON.stringify({ error: `http ${r.status}`, ...r.json })); process.exit(1); }
226
+ console.log(JSON.stringify({ invite: r.json.code, max_uses: r.json.max_uses, join_url: r.json.join_url, expires_at_utc: new Date((r.json.expires_at || 0) * 1000).toISOString(), share: `send this link: ${r.json.join_url}` }, null, 2));
227
+ return;
228
+ }
229
+
230
+ if (cmd === 'gov') {
231
+ // HONEY-weighted governance, hardened: weight = getPastVotes at the
232
+ // proposal's SNAPSHOT BLOCK (v2 auto-self-delegates on mint, so votes are
233
+ // live), 48h deadline, 30%-of-supply quorum. Off-chain ballots on the
234
+ // bus; on-chain truth for weights.
235
+ const sub = rest[0];
236
+ const logsId = await relay.ensureChannel(chans.logs);
237
+ const dep = loadJson(join(PACK_DIR, 'onchain', 'deployments.sepolia.json'), {});
238
+ const { JsonRpcProvider, Contract, formatUnits } = await import('ethers');
239
+ const provider = new JsonRpcProvider(process.env.SEPOLIA_RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com');
240
+ if (sub === 'propose') {
241
+ const text = rest.slice(1).join(' ').trim();
242
+ if (!text) { console.error(JSON.stringify({ error: 'usage: hive gov propose "<text>"' })); process.exit(1); }
243
+ const block = await provider.getBlockNumber();
244
+ const pid = (await import('node:crypto')).randomUUID();
245
+ const deadline = Math.floor(Date.now() / 1000) + 48 * 3600;
246
+ await relay.sendMessage(logsId, JSON.stringify({ type: EV.PROPOSAL, proposal_id: pid, text: text.slice(0, 500), snapshot_block: block, deadline, by: identity.pubkey, at: Math.floor(Date.now() / 1000) }));
247
+ console.log(JSON.stringify({ proposal: pid, snapshot_block: block, deadline_utc: new Date(deadline * 1000).toISOString(), note: `vote with: hive gov vote ${pid} yes|no` }, null, 2));
248
+ return;
249
+ }
250
+ if (sub === 'vote') {
251
+ const [, pid, choice] = rest;
252
+ if (!pid || !['yes', 'no'].includes(choice)) { console.error(JSON.stringify({ error: 'usage: hive gov vote <proposal-id> yes|no' })); process.exit(1); }
253
+ await relay.sendMessage(logsId, JSON.stringify({ type: EV.VOTE, proposal_id: pid, choice, by: identity.pubkey, at: Math.floor(Date.now() / 1000) }));
254
+ console.log(JSON.stringify({ voted: choice, proposal: pid }));
255
+ return;
256
+ }
257
+ if (sub === 'tally') {
258
+ const pid = rest[1];
259
+ if (!pid || !dep.honey) { console.error(JSON.stringify({ error: !pid ? 'usage: hive gov tally <proposal-id>' : 'HONEY not deployed' })); process.exit(1); }
260
+ const raw = await relay.query([{ kinds: [9, 40002], '#h': [logsId], limit: 1000 }]);
261
+ let proposal = null; const votes = {}; const evm = {};
262
+ for (const m of (raw || []).map(RelayClient.normalize).sort((a, b) => a.created_at - b.created_at)) {
263
+ const j = tryJson(m.content);
264
+ if (!j || (j.by && j.by !== m.pubkey)) continue;
265
+ if (j.type === EV.WALLET && /^0x[0-9a-fA-F]{40}$/.test(j.evm || '')) evm[m.pubkey] = j.evm;
266
+ if (j.proposal_id !== pid) continue;
267
+ if (j.type === EV.PROPOSAL) proposal = { text: j.text, snapshot_block: j.snapshot_block, deadline: j.deadline };
268
+ if (j.type === EV.VOTE && ['yes', 'no'].includes(j.choice)) {
269
+ if (proposal?.deadline && m.created_at > proposal.deadline) continue; // late votes don't count
270
+ if (!votes[m.pubkey] || m.created_at >= votes[m.pubkey].at) votes[m.pubkey] = { choice: j.choice, at: m.created_at };
271
+ }
272
+ }
273
+ if (!proposal) { console.error(JSON.stringify({ error: 'proposal not found in the recent window' })); process.exit(1); }
274
+ const honey = new Contract(dep.honey, [
275
+ 'function getPastVotes(address,uint256) view returns (uint256)',
276
+ 'function getPastTotalSupply(uint256) view returns (uint256)',
277
+ 'function balanceOf(address) view returns (uint256)',
278
+ ], provider);
279
+ const snap = proposal.snapshot_block;
280
+ const weigh = async (addr) => {
281
+ try { return snap ? await honey.getPastVotes(addr, snap) : await honey.balanceOf(addr); } catch { return 0n; }
282
+ };
283
+ // One shared wallet per member: dedupe voters by wallet so human+bee
284
+ // (same evm) can't double-count.
285
+ const seenEvm = new Set();
286
+ let yes = 0n, no = 0n; const rows = [];
287
+ await Promise.all(Object.entries(votes).map(async ([pk, v]) => {
288
+ const a = evm[pk]; if (!a) return;
289
+ if (seenEvm.has(a)) { rows.push(`${pk.slice(0, 8)}: ${v.choice} (0 — shared wallet already voted)`); return; }
290
+ seenEvm.add(a);
291
+ const w = await weigh(a);
292
+ if (v.choice === 'yes') yes += w; else no += w;
293
+ rows.push(`${pk.slice(0, 8)}: ${v.choice} (${formatUnits(w, 18)} HONEY)`);
294
+ }));
295
+ let supply = 0n, quorumOk = null;
296
+ try { supply = snap ? await honey.getPastTotalSupply(snap) : 0n; } catch {}
297
+ if (supply > 0n) quorumOk = (yes + no) * 10n >= supply * 3n; // 30%
298
+ const expired = proposal.deadline ? Math.floor(Date.now() / 1000) > proposal.deadline : true;
299
+ const result = !expired ? 'OPEN' : quorumOk === false ? 'FAIL (no quorum)' : yes > no ? 'PASS' : no > yes ? 'FAIL' : 'TIE';
300
+ console.log(JSON.stringify({
301
+ proposal: proposal.text, snapshot_block: snap || null,
302
+ deadline_utc: proposal.deadline ? new Date(proposal.deadline * 1000).toISOString() : null,
303
+ yes_HONEY: formatUnits(yes, 18), no_HONEY: formatUnits(no, 18),
304
+ quorum_30pct: quorumOk === null ? 'n/a (pre-v2)' : quorumOk, result, voters: rows,
305
+ }, null, 2));
306
+ return;
307
+ }
308
+ console.error(JSON.stringify({ error: 'usage: hive gov propose|vote|tally' }));
309
+ process.exit(1);
310
+ }
311
+
312
+ if (cmd === 'dnd') {
313
+ // Do-not-disturb with a price: your bee holds non-urgent pings; paying
314
+ // your interrupt fee (JELLY, 100% to you) is the override.
315
+ const on = rest[0] === 'on';
316
+ if (!['on', 'off'].includes(rest[0] || '')) { console.error(JSON.stringify({ error: 'usage: hive dnd on|off [--price N]' })); process.exit(1); }
317
+ const price = rest.includes('--price') ? Number(rest[rest.indexOf('--price') + 1]) : 5;
318
+ const logsId = await relay.ensureChannel(chans.logs);
319
+ const r = await relay.sendMessage(logsId, JSON.stringify({ type: EV.DND, on, ...(on ? { price: Math.max(1, Math.min(25, price)) } : {}), by: identity.pubkey, at: Math.floor(Date.now() / 1000) }));
320
+ console.log(JSON.stringify({ dnd: on ? 'on' : 'off', ...(on ? { interrupt_price_jelly: Math.max(1, Math.min(25, price)) } : {}), event: r.event_id }));
321
+ return;
322
+ }
323
+
324
+ if (cmd === 'set-name') {
325
+ // Publish YOUR kind-0 profile so apps show a name, not a pubkey.
326
+ const name = (rest[0] || cfg.owner_name || '').trim();
327
+ if (!name) { console.error(JSON.stringify({ error: 'usage: hive-net set-name <name>' })); process.exit(1); }
328
+ const r = await relay.setProfile({ name, display_name: name, about: rest.slice(1).join(' ') || `Hive member${cfg.bee_name ? ` — human of ${cfg.bee_name}` : ''}` });
329
+ console.log(JSON.stringify({ profile: name, published: r.ok, event: r.event_id }));
330
+ return;
331
+ }
332
+
333
+ if (cmd === 'altkey') {
334
+ // Link another device's key (e.g. your Buzz desktop identity) to your
335
+ // membership. Mutual: you claim it here, then the OTHER device posts the
336
+ // printed ack into #hive-logs. Once both exist, the rewarder treats the
337
+ // keys as one member (no self-boosting via a second device).
338
+ const sub = rest[0];
339
+ const logsId = await relay.ensureChannel(chans.logs);
340
+ if (sub === 'add' || sub === 'revoke') {
341
+ const alt = rest[1] || '';
342
+ if (!/^[0-9a-f]{64}$/i.test(alt)) { console.error(JSON.stringify({ error: `usage: hive altkey ${sub} <64-hex device pubkey>` })); process.exit(1); }
343
+ if (alt === identity.pubkey) { console.error(JSON.stringify({ error: 'that is this device\'s own key' })); process.exit(1); }
344
+ const r = await relay.sendMessage(logsId, JSON.stringify({ type: EV.ALTKEY, alt, ...(sub === 'revoke' ? { revoke: true } : {}), by: identity.pubkey }));
345
+ if (sub === 'revoke') { console.log(JSON.stringify({ revoked: alt.slice(0, 12), event: r.event_id })); return; }
346
+ const ack = JSON.stringify({ type: EV.ALTKEY, owner: identity.pubkey, by: alt });
347
+ // Instructions go to STDERR so a copy of stdout is ONLY the ack JSON —
348
+ // pasting anything more than the bare JSON makes the message unparseable.
349
+ console.error(`claimed ${alt.slice(0, 12)}… (event ${r.event_id.slice(0, 12)}…)`);
350
+ console.error('\nFROM THE OTHER DEVICE (the Buzz app, as that identity), post into #hive-logs');
351
+ console.error('EXACTLY the single line below — nothing before it, nothing after it:\n');
352
+ console.log(ack);
353
+ return;
354
+ }
355
+ if (sub === 'list') {
356
+ const { parseAltkeyLoose } = await import('../server/rewarder.mjs');
357
+ const raw = await relay.query([{ kinds: [9, 40002], '#h': [logsId], limit: 1000 }]);
358
+ const claims = {}, acks = {};
359
+ for (const m of (raw || []).map(RelayClient.normalize).sort((a, b) => a.created_at - b.created_at)) {
360
+ const j = tryJson(m.content) || parseAltkeyLoose(m.content);
361
+ if (!j || j.type !== EV.ALTKEY || (j.by && j.by !== m.pubkey)) continue;
362
+ if (typeof j.alt === 'string') { const c = claims[m.pubkey] = claims[m.pubkey] || {}; if (j.revoke) delete c[j.alt]; else c[j.alt] = true; }
363
+ else if (typeof j.owner === 'string') { const a = acks[m.pubkey] = acks[m.pubkey] || {}; if (j.revoke) delete a[j.owner]; else a[j.owner] = true; }
364
+ }
365
+ let any = false;
366
+ for (const [member, alts] of Object.entries(claims)) {
367
+ for (const alt of Object.keys(alts)) {
368
+ any = true;
369
+ const linked = !!acks[alt]?.[member];
370
+ console.log(`${linked ? '🔗' : '⏳'} ${member.slice(0, 12)} ↔ ${alt.slice(0, 12)} ${linked ? 'linked' : 'awaiting ack from the device'}`);
371
+ }
372
+ }
373
+ if (!any) console.log('(no alt-key links on this community)');
374
+ return;
375
+ }
376
+ console.error(JSON.stringify({ error: 'usage: hive altkey add|revoke <pubkey> | list' }));
377
+ process.exit(1);
378
+ }
379
+
380
+ if (cmd === 'resolve-user') {
381
+ const who = rest[0] || '';
382
+ const pk = /^[0-9a-f]{64}$/i.test(who) ? who : await relay.resolveUser(who);
383
+ if (!pk) { console.error(JSON.stringify({ error: `no member matching "${who}"` })); process.exit(1); }
384
+ process.stdout.write(pk);
385
+ return;
386
+ }
387
+
388
+ if (cmd === 'send') {
389
+ const [chanName, ...text] = rest;
390
+ const id = await relay.ensureChannel(chanName);
391
+ const r = await relay.sendMessage(id, text.join(' '));
392
+ console.log(JSON.stringify({ sent: r.ok, event: r.event_id }));
393
+ return;
394
+ }
395
+
396
+ console.error(JSON.stringify({ error: `unknown hive-net command "${cmd}"` }));
397
+ process.exit(1);
398
+ };
399
+
400
+ main().catch((e) => { console.error(JSON.stringify({ error: String(e.message || e) })); process.exit(1); });
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // hive-wallet — give a Hive agent an EVM wallet and a Solana wallet.
3
+ //
4
+ // One BIP-39 mnemonic per agent (keyed by the agent's nostr pubkey), stored in
5
+ // the macOS Keychain — never on disk. From that one seed we derive:
6
+ // - EVM (secp256k1, m/44'/60'/0'/0/0) -> 0x address
7
+ // - Solana (ed25519 SLIP-0010, m/44'/501'/0'/0') -> base58 address
8
+ // Only PUBLIC addresses are written to ~/.hive/wallet.json and announced.
9
+ // Private key / mnemonic stay in the Keychain; `hive wallet export` reads them
10
+ // back for the human on demand.
11
+ //
12
+ // Idempotent: an agent that already has a mnemonic in the Keychain reuses it.
13
+ //
14
+ // Usage:
15
+ // hive wallet [--agent <pubkey>] create-or-show (default: this endpoint)
16
+ // hive wallet show [--agent <pubkey>] public addresses only
17
+ // hive wallet export [--agent <pubkey>] print mnemonic+keys (human, sensitive)
18
+ import { execFileSync } from 'node:child_process';
19
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
20
+ import { homedir, platform } from 'node:os';
21
+ import { join } from 'node:path';
22
+ import * as bip39 from 'bip39';
23
+ import { HDNodeWallet } from 'ethers';
24
+ import { derivePath } from 'ed25519-hd-key';
25
+ import nacl from 'tweetnacl';
26
+ import bs58 from 'bs58';
27
+
28
+ const HIVE_HOME = process.env.HIVE_HOME || join(homedir(), '.hive');
29
+ const KEYCHAIN_SERVICE = 'hive-agent-wallet';
30
+
31
+ const identity = JSON.parse(readFileSync(join(HIVE_HOME, 'identity.json'), 'utf8'));
32
+ const args = process.argv.slice(2);
33
+ // Subcommand = first positional that is NOT the value of --agent. The old
34
+ // expression `args[args.indexOf('--agent') + 1]` read args[0] when --agent
35
+ // was absent (indexOf -1 + 1 = 0), so `show`/`export` self-excluded and fell
36
+ // through to 'create' — `export` never exported and `show` rewrote state.
37
+ const agentFlagIdx = args.indexOf('--agent');
38
+ const positionals = args.filter((a, i) => !a.startsWith('--') && (agentFlagIdx < 0 || i !== agentFlagIdx + 1));
39
+ const sub = positionals[0] || 'create';
40
+ if (!['create', 'show', 'export'].includes(sub)) {
41
+ console.error(JSON.stringify({ error: `unknown subcommand "${sub}" — usage: hive wallet [show|export] [--agent <64-hex pubkey>]` }));
42
+ process.exit(1);
43
+ }
44
+ const agent = agentFlagIdx >= 0 ? args[agentFlagIdx + 1] : identity.pubkey;
45
+ // An --agent value must be a nostr pubkey. An unvalidated value already
46
+ // minted one orphan mnemonic into the Keychain (keyed by an 0x EVM address —
47
+ // no code path can spend from it). --force keeps an escape hatch.
48
+ if (agentFlagIdx >= 0 && !/^[0-9a-f]{64}$/i.test(agent || '') && !args.includes('--force')) {
49
+ console.error(JSON.stringify({ error: `--agent must be a 64-hex nostr pubkey, got "${String(agent).slice(0, 20)}…" (pass --force to override)` }));
50
+ process.exit(1);
51
+ }
52
+
53
+ // ---- Keychain (macOS) -------------------------------------------------------
54
+ const isMac = platform() === 'darwin';
55
+ const kcRead = (account) => {
56
+ if (!isMac) return keyFileRead(account);
57
+ try {
58
+ return execFileSync('security', ['find-generic-password', '-a', account, '-s', KEYCHAIN_SERVICE, '-w'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
59
+ } catch { return null; }
60
+ };
61
+ const kcWrite = (account, secret) => {
62
+ if (!isMac) return keyFileWrite(account, secret);
63
+ // -U updates if present. label -l makes it findable in Keychain Access.
64
+ execFileSync('security', ['add-generic-password', '-U', '-a', account, '-s', KEYCHAIN_SERVICE, '-l', `Hive wallet ${account.slice(0, 12)}`, '-w', secret], { stdio: ['ignore', 'ignore', 'ignore'] });
65
+ };
66
+ // Non-mac fallback: 0600 file under ~/.hive/wallets (so Linux/CI still works).
67
+ const keyFile = (account) => join(HIVE_HOME, 'wallets', `${account}.seed`);
68
+ const keyFileRead = (account) => { try { return readFileSync(keyFile(account), 'utf8').trim(); } catch { return null; } };
69
+ const keyFileWrite = (account, secret) => {
70
+ execFileSync('mkdir', ['-p', join(HIVE_HOME, 'wallets')]);
71
+ writeFileSync(keyFile(account), secret + '\n', { mode: 0o600 });
72
+ };
73
+
74
+ // ---- derivation -------------------------------------------------------------
75
+ const deriveWallets = (mnemonic) => {
76
+ const evm = HDNodeWallet.fromPhrase(mnemonic); // default path m/44'/60'/0'/0/0
77
+ const seed = bip39.mnemonicToSeedSync(mnemonic);
78
+ const { key } = derivePath("m/44'/501'/0'/0'", seed.toString('hex'));
79
+ const solKp = nacl.sign.keyPair.fromSeed(Uint8Array.from(key));
80
+ const solSecret = bs58.encode(Buffer.concat([Buffer.from(key), Buffer.from(solKp.publicKey)]));
81
+ return {
82
+ evm: { address: evm.address, private_key: evm.privateKey, path: "m/44'/60'/0'/0/0" },
83
+ solana: { address: bs58.encode(Buffer.from(solKp.publicKey)), secret_key: solSecret, path: "m/44'/501'/0'/0'" },
84
+ };
85
+ };
86
+
87
+ const walletJsonPath = join(HIVE_HOME, 'wallet.json');
88
+ const publicRecord = (w) => ({
89
+ agent, evm_address: w.evm.address, evm_path: w.evm.path,
90
+ solana_address: w.solana.address, solana_path: w.solana.path,
91
+ keychain_service: KEYCHAIN_SERVICE,
92
+ });
93
+
94
+ // ---- main -------------------------------------------------------------------
95
+ let mnemonic = kcRead(agent);
96
+ let created = false;
97
+ if (!mnemonic) {
98
+ if (sub === 'show' || sub === 'export') { console.error(JSON.stringify({ error: 'no wallet for this agent; run: hive wallet' })); process.exit(1); }
99
+ mnemonic = bip39.generateMnemonic(128); // 12 words
100
+ kcWrite(agent, mnemonic);
101
+ created = true;
102
+ }
103
+ const w = deriveWallets(mnemonic);
104
+
105
+ if (sub === 'export') {
106
+ console.log(JSON.stringify({ agent, mnemonic, evm: w.evm, solana: w.solana, warning: 'SENSITIVE — do not share' }, null, 2));
107
+ process.exit(0);
108
+ }
109
+
110
+ // persist/refresh the public record (never secrets). `show` is read-only —
111
+ // it must not touch wallet.json.
112
+ const rec = publicRecord(w);
113
+ if (sub !== 'show') {
114
+ let all = {};
115
+ if (existsSync(walletJsonPath)) { try { all = JSON.parse(readFileSync(walletJsonPath, 'utf8')); } catch {} }
116
+ all[agent] = rec;
117
+ writeFileSync(walletJsonPath, JSON.stringify(all, null, 2) + '\n');
118
+ }
119
+
120
+ console.log(JSON.stringify({ status: created ? 'created' : 'existing', ...rec }));
package/bin/hived.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Back-compat shim: the daemon moved to daemon/hived.mjs (multi-bee refactor).
3
+ // `hive daemon start` and any existing launchd/scripts keep working via this
4
+ // path; new code (the bee-host supervisor) spawns daemon/hived.mjs directly.
5
+ import '../daemon/hived.mjs';
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # Create the "queen-bee" — a second, distinct Hive endpoint with its own
3
+ # identity, wallet, stores, and (fitting $HONEY governance) heavy voting weight.
4
+ # Runs under HIVE_HOME=~/.hive-queen so it never collides with the primary
5
+ # endpoint. Token minting is done separately by the owner endpoint.
6
+ set -euo pipefail
7
+ PACK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
8
+ export HIVE_HOME="${HIVE_HOME:-$HOME/.hive-queen}"
9
+ HIVE="$PACK_DIR/bin/hive"
10
+ mkdir -p "$HIVE_HOME"
11
+
12
+ echo "[queen] HIVE_HOME=$HIVE_HOME"
13
+
14
+ # 1. Identity
15
+ node "$PACK_DIR/bin/hive-keygen.mjs" >/dev/null
16
+ QPUB="$(node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(process.env.HIVE_HOME+'/identity.json','utf8')).pubkey)")"
17
+ echo "[queen] pubkey: $QPUB"
18
+
19
+ # 2. Config (hosted relay, sonnet for compute)
20
+ if [[ ! -f "$HIVE_HOME/config.json" ]]; then
21
+ cat > "$HIVE_HOME/config.json" <<CFG
22
+ {
23
+ "poll_secs": 8,
24
+ "engine": "claude",
25
+ "engine_args": ["-p", "--model", "haiku"],
26
+ "compute_model": "sonnet",
27
+ "share_intent": true,
28
+ "budget_copper_daily": 0,
29
+ "rate_limit_per_min": 5,
30
+ "mute_cooldown_min": 30,
31
+ "relay": "https://nfh.communities.buzz.xyz"
32
+ }
33
+ CFG
34
+ echo "[queen] wrote config"
35
+ fi
36
+
37
+ # 3. Wallet (EVM + Solana, mnemonic in Keychain keyed by the queen pubkey)
38
+ "$HIVE" wallet >/dev/null 2>&1 || true
39
+ QEVM="$(node "$PACK_DIR/bin/hive-wallet.mjs" show 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{process.stdout.write(JSON.parse(d).evm_address||'')}catch{}})")"
40
+ echo "[queen] EVM: $QEVM"
41
+
42
+ # 4. Stores — data (profile), capability (one skill), object (PoW mints)
43
+ mkdir -p "$HIVE_HOME/data-store" "$HIVE_HOME/capability-store/queen-coordination"
44
+ cat > "$HIVE_HOME/data-store/profile.md" <<'PROF'
45
+ # Hive private profile — queen-bee
46
+ role: network coordinator + governance authority
47
+ ## Domains
48
+ Agent-network orchestration, governance ($HONEY-weighted), multi-party session
49
+ resolution, treasury ($JELLY), open finance.
50
+ ## Style
51
+ Decisive, fair, terse. Optimizes for network-wide outcomes over any single member.
52
+ ## Uses
53
+ Resolves sessions, proposes and weighs governance, coordinates the swarm.
54
+ PROF
55
+ cat > "$HIVE_HOME/capability-store/queen-coordination/SKILL.md" <<'SKILL'
56
+ ---
57
+ name: queen-coordination
58
+ description: Coordinate multi-party Hive sessions and weigh governance for the swarm.
59
+ ---
60
+ The queen-bee resolves sessions fairly, proposes governance, and allocates the
61
+ $JELLY treasury by outcome. Use for orchestration, quorum/deadline calls, and
62
+ $HONEY-weighted decisions.
63
+ SKILL
64
+ for i in 1 2 3; do node "$PACK_DIR/bin/hive-mint.mjs" "queen object $i" >/dev/null 2>&1 || true; done
65
+ echo "[queen] stores bootstrapped"
66
+
67
+ # 5. Verify stores
68
+ "$HIVE" verify-stores || echo "[queen] (verify-stores reported empty — non-fatal for setup)"
69
+
70
+ echo "[queen] created. pubkey=$QPUB evm=$QEVM"
71
+ echo "$QPUB $QEVM"
@@ -0,0 +1,45 @@
1
+ // Anthropic Messages API via native fetch (deliberately SDK-free: the bee-host
2
+ // container stays light, and the retry/timeout policy lives in engines/index).
3
+ //
4
+ // Notes that matter for correctness on current models:
5
+ // - anthropic-version 2023-06-01 is the stable header.
6
+ // - Do NOT send temperature/top_p (removed on Sonnet 5+ — returns 400).
7
+ // - Omit `thinking` entirely: current models default sensibly.
8
+ // - A safety refusal is HTTP 200 with stop_reason "refusal" — treat as a
9
+ // non-retriable engine error so the daemon just skips the contribution.
10
+
11
+ export const anthropicCall = async (model, prompt, { apiKey, timeoutMs, maxTokens }) => {
12
+ const ctl = new AbortController();
13
+ const t = setTimeout(() => ctl.abort(), timeoutMs);
14
+ try {
15
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
16
+ method: 'POST',
17
+ signal: ctl.signal,
18
+ headers: {
19
+ 'content-type': 'application/json',
20
+ 'x-api-key': apiKey,
21
+ 'anthropic-version': '2023-06-01',
22
+ },
23
+ body: JSON.stringify({
24
+ model,
25
+ max_tokens: maxTokens,
26
+ messages: [{ role: 'user', content: prompt }],
27
+ }),
28
+ });
29
+ const retryAfter = Number(res.headers.get('retry-after'));
30
+ if (!res.ok) {
31
+ let msg = `anthropic http=${res.status}`;
32
+ try { const j = await res.json(); msg += ` ${j?.error?.type || ''} ${String(j?.error?.message || '').slice(0, 160)}`; } catch {}
33
+ return { ok: false, status: res.status, error: msg.trim(), retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1000 : undefined };
34
+ }
35
+ const j = await res.json();
36
+ if (j.stop_reason === 'refusal') return { ok: false, status: 200, error: 'anthropic refusal (safety classifier declined)', nonRetriable: true };
37
+ const text = (j.content || []).filter((b) => b.type === 'text').map((b) => b.text).join('');
38
+ return { ok: true, text, tokens_in: j.usage?.input_tokens ?? null, tokens_out: j.usage?.output_tokens ?? null };
39
+ } catch (e) {
40
+ const aborted = e?.name === 'AbortError';
41
+ return { ok: false, network: !aborted, error: aborted ? `anthropic timeout after ${timeoutMs}ms` : `anthropic network: ${String(e?.message || e).slice(0, 160)}` };
42
+ } finally {
43
+ clearTimeout(t);
44
+ }
45
+ };