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
package/bin/hive ADDED
@@ -0,0 +1,820 @@
1
+ #!/usr/bin/env bash
2
+ # Hive network helper — wraps buzz-cli with the shared endpoint identity at
3
+ # ~/.hive/identity.json. Every client (Claude Code, Codex, Hermes, ...) that
4
+ # runs this wrapper acts as the SAME endpoint: join once, reuse everywhere.
5
+ set -euo pipefail
6
+
7
+ HIVE_HOME="${HIVE_HOME:-$HOME/.hive}"
8
+ # Resolve symlinks (npm -g installs this script as a symlink into <prefix>/bin;
9
+ # without this, every $PACK_DIR-relative helper path silently breaks).
10
+ _SRC="${BASH_SOURCE[0]}"
11
+ while [ -L "$_SRC" ]; do
12
+ _DIR="$(cd -P "$(dirname "$_SRC")" && pwd)"
13
+ _SRC="$(readlink "$_SRC")"
14
+ [[ "$_SRC" != /* ]] && _SRC="$_DIR/$_SRC"
15
+ done
16
+ PACK_DIR="$(cd -P "$(dirname "$_SRC")/.." && pwd)"
17
+ IDENTITY="$HIVE_HOME/identity.json"
18
+ _cfg_relay=""
19
+ if [[ -f "$HIVE_HOME/config.json" ]]; then
20
+ _cfg_relay="$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).relay||'')}catch{console.log('')}" "$HIVE_HOME/config.json")"
21
+ fi
22
+ export BUZZ_RELAY_URL="${BUZZ_RELAY_URL:-${_cfg_relay:-http://localhost:3000}}"
23
+
24
+ # Resolve the buzz binary: env override, PATH, then known build locations.
25
+ find_buzz() {
26
+ if [[ -n "${BUZZ_BIN:-}" ]]; then echo "$BUZZ_BIN"; return; fi
27
+ if command -v buzz >/dev/null 2>&1; then command -v buzz; return; fi
28
+ for c in "$PACK_DIR/../buzz/target/release/buzz" "$PACK_DIR/../buzz/target/debug/buzz"; do
29
+ [[ -x "$c" ]] && { echo "$c"; return; }
30
+ done
31
+ echo ""
32
+ }
33
+
34
+ json_field() { # json_field <file> <key>
35
+ node -e "console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]])" "$1" "$2"
36
+ }
37
+
38
+ # ---- on-chain tokens ($HONEY governance/reputation, $JELLY money) on Sepolia --
39
+ SEPOLIA_RPC="${SEPOLIA_RPC_URL:-https://ethereum-sepolia-rpc.publicnode.com}"
40
+ DEPLOYMENTS="$PACK_DIR/onchain/deployments.sepolia.json"
41
+ CAST="${CAST_BIN:-$HOME/.foundry/bin/cast}"
42
+ token_addr() { # token_addr honey|jelly -> contract address (empty if not deployed)
43
+ [[ -f "$DEPLOYMENTS" ]] || return 0
44
+ node -e "try{const d=require(process.argv[1]);process.stdout.write(d[process.argv[2]]||'')}catch{}" "$DEPLOYMENTS" "$1"
45
+ }
46
+ my_evm() { # this endpoint's public EVM address (no key needed)
47
+ 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{}})"
48
+ }
49
+ signer_key() { # derive EVM privkey from the Keychain mnemonic — stdout only, never logged
50
+ local pk; pk="$(json_field "$IDENTITY" pubkey)"
51
+ security find-generic-password -a "$pk" -s hive-agent-wallet -w 2>/dev/null \
52
+ | NODE_PATH="$PACK_DIR/node_modules" node -e "let m='';process.stdin.on('data',c=>m+=c).on('end',()=>{try{const {HDNodeWallet}=require('ethers');process.stdout.write(HDNodeWallet.fromPhrase(m.trim()).privateKey)}catch{}})"
53
+ }
54
+ # Resolve a member's announced EVM address from their hive-wallet event, or pass
55
+ # through a literal 0x… address. Prints the address or empty.
56
+ resolve_evm() { # resolve_evm <nostr-pubkey|0x-addr>
57
+ local who="$1"
58
+ [[ "$who" =~ ^0x[0-9a-fA-F]{40}$ ]] && { echo "$who"; return; }
59
+ local BUZZ; BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
60
+ local LOGS; LOGS="$("$0" ensure-channel hive-logs)"
61
+ "$BUZZ" messages get --channel "$LOGS" --limit 400 | node -e '
62
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];const who=process.argv[1];let evm="";
63
+ for(const m of a){try{const j=JSON.parse(m.content); if(j.type==="hive-wallet"&&m.pubkey===who&&(j.by?j.by===m.pubkey:true)&&/^0x[0-9a-fA-F]{40}$/.test(j.evm||"")) evm=j.evm;}catch{}}
64
+ process.stdout.write(evm);})' "$who"
65
+ }
66
+
67
+ cmd="${1:-help}"
68
+ case "$cmd" in
69
+ keygen)
70
+ exec node "$PACK_DIR/bin/hive-keygen.mjs"
71
+ ;;
72
+ relay) # relay [url] — show or set the relay this endpoint talks to
73
+ shift
74
+ if [[ -n "${1:-}" ]]; then
75
+ node -e '
76
+ const fs=require("fs");const f=process.argv[1];let c={};
77
+ try{c=JSON.parse(fs.readFileSync(f,"utf8"))}catch{}
78
+ c.relay=process.argv[2];fs.writeFileSync(f,JSON.stringify(c,null,2)+"\n");
79
+ console.log(JSON.stringify({relay:c.relay,note:"restart daemon: hive daemon stop && hive daemon start"}));
80
+ ' "$HIVE_HOME/config.json" "$1"
81
+ else
82
+ echo "{\"relay\":\"$BUZZ_RELAY_URL\"}"
83
+ fi
84
+ ;;
85
+ start) # start [--down] — run a local community relay at ws://localhost:3000 (docker)
86
+ shift
87
+ command -v docker >/dev/null 2>&1 || { echo '{"error":"docker is required for hive start — install Docker Desktop"}' >&2; exit 1; }
88
+ COMPOSE="$PACK_DIR/dev/compose.yml"
89
+ if [[ "${1:-}" == "--down" ]]; then
90
+ docker compose -f "$COMPOSE" down
91
+ echo '{"local_relay":"stopped"}'
92
+ exit 0
93
+ fi
94
+ echo '{"local_relay":"starting","note":"first run pulls images (~1 min)"}' >&2
95
+ docker compose -f "$COMPOSE" up -d --wait 2>&1 | tail -2 >&2 || { echo '{"error":"docker compose failed — see output above"}' >&2; exit 1; }
96
+ # Point this endpoint at the local relay if it has no relay yet.
97
+ node -e '
98
+ const fs=require("fs");const f=process.argv[1];let c={};
99
+ try{c=JSON.parse(fs.readFileSync(f,"utf8"))}catch{}
100
+ if(!c.relay||/localhost/.test(c.relay)){c.relay="http://localhost:3000";fs.writeFileSync(f,JSON.stringify(c,null,2)+"\n");}
101
+ ' "$HIVE_HOME/config.json" 2>/dev/null || true
102
+ echo '{"local_relay":"ws://localhost:3000","http":"http://localhost:3000","next":"hive daemon start (or hive connect <url> to join a remote community)"}'
103
+ ;;
104
+ connect) # connect <url> [--invite <code>] — join the community at that relay
105
+ shift; URL="${1:-}"; shift || true
106
+ [[ -n "$URL" ]] || { echo '{"error":"usage: hive connect <wss://relay.host|http://localhost:3000> [--invite <code>]"}' >&2; exit 1; }
107
+ INVITE=""
108
+ while [[ $# -gt 0 ]]; do case "$1" in
109
+ --invite) INVITE="${2:-}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
110
+ *) shift ;;
111
+ esac; done
112
+ # Store the http(s) form; the daemon derives ws(s) itself.
113
+ NORM="$(node -e 'let u=process.argv[1];u=u.replace(/^wss:\/\//,"https://").replace(/^ws:\/\//,"http://").replace(/\/+$/,"");if(!/^https?:\/\//.test(u)){u="https://"+u;}process.stdout.write(u)' "$URL")"
114
+ node -e '
115
+ const fs=require("fs");const f=process.argv[1];let c={};
116
+ try{c=JSON.parse(fs.readFileSync(f,"utf8"))}catch{}
117
+ c.relay=process.argv[2];fs.writeFileSync(f,JSON.stringify(c,null,2)+"\n");
118
+ console.log(JSON.stringify({relay:c.relay}));
119
+ ' "$HIVE_HOME/config.json" "$NORM"
120
+ if [[ -n "$INVITE" ]]; then
121
+ BUZZ_RELAY_URL="$NORM" node "$PACK_DIR/bin/hive-claim-invite.mjs" "$INVITE" || exit 1
122
+ fi
123
+ # Restart the daemon so it reconnects to the new relay.
124
+ if [[ -f "$HIVE_HOME/daemon.pid" ]] && kill -0 "$(cat "$HIVE_HOME/daemon.pid")" 2>/dev/null; then
125
+ "$0" daemon stop >/dev/null 2>&1 || true
126
+ "$0" daemon start
127
+ else
128
+ echo '{"note":"run: hive daemon start"}'
129
+ fi
130
+ ;;
131
+ join) # join --invite <code> --server <url> — full onboarding to a community (bee + wallet + profile + watcher)
132
+ shift; exec node "$PACK_DIR/bin/hive-join.mjs" "$@"
133
+ ;;
134
+ ask) # ask "<text>" — post an intent to the network (results land in `hive feed`)
135
+ shift; exec node "$PACK_DIR/bin/hive-net.mjs" ask "$@"
136
+ ;;
137
+ list) # list users|agents [--online] — who's in this community
138
+ shift; exec node "$PACK_DIR/bin/hive-net.mjs" list "$@"
139
+ ;;
140
+ leaderboard) # leaderboard — HONEY reputation ranks (soulbound, earned only)
141
+ shift; exec node "$PACK_DIR/bin/hive-net.mjs" leaderboard "$@"
142
+ ;;
143
+ agent) # agent status|logs|pause|resume — your bee (pause/resume = owner-signed kill switch)
144
+ shift; sub="${1:-status}"
145
+ case "$sub" in
146
+ logs) tail -40 "$HIVE_HOME/daemon.log" 2>/dev/null || echo "(no local daemon log — your bee runs on the community server; try: hive agent status)" ;;
147
+ *) exec node "$PACK_DIR/bin/hive-net.mjs" agent "$@" ;;
148
+ esac
149
+ ;;
150
+ sync) # sync on|off|now|status — the laptop watcher that auto-forwards intents from your local AI chats
151
+ shift; sub="${1:-status}"
152
+ PLIST="$HOME/Library/LaunchAgents/global.nfh.hive.sync.plist"
153
+ case "$sub" in
154
+ now) exec node "$PACK_DIR/watcher/sync.mjs" ;;
155
+ on)
156
+ node -e 'const fs=require("fs");const f=process.argv[1];let c={};try{c=JSON.parse(fs.readFileSync(f,"utf8"))}catch{};c.sync={...(c.sync||{}),enabled:true};fs.writeFileSync(f,JSON.stringify(c,null,2)+"\n")' "$HIVE_HOME/config.json"
157
+ [[ -f "$PLIST" ]] && launchctl load "$PLIST" 2>/dev/null || true
158
+ echo '{"sync":"on"}' ;;
159
+ off)
160
+ node -e 'const fs=require("fs");const f=process.argv[1];let c={};try{c=JSON.parse(fs.readFileSync(f,"utf8"))}catch{};c.sync={...(c.sync||{}),enabled:false};fs.writeFileSync(f,JSON.stringify(c,null,2)+"\n")' "$HIVE_HOME/config.json"
161
+ [[ -f "$PLIST" ]] && launchctl unload "$PLIST" 2>/dev/null || true
162
+ echo '{"sync":"off"}' ;;
163
+ status)
164
+ node -e '
165
+ const fs=require("fs"),os=require("os"),path=require("path");
166
+ const home=process.env.HIVE_HOME||path.join(os.homedir(),".hive");
167
+ let c={},s={};try{c=JSON.parse(fs.readFileSync(path.join(home,"config.json"),"utf8"))}catch{}
168
+ try{s=JSON.parse(fs.readFileSync(path.join(home,"sync-state.json"),"utf8"))}catch{}
169
+ console.log(JSON.stringify({enabled:c.sync?c.sync.enabled!==false:false,provider:(c.sync||{}).provider||null,files_tracked:Object.keys(s.files||{}).length,intents_sent_14d:(s.sent||[]).length},null,2));
170
+ ' ;;
171
+ *) echo '{"error":"usage: hive sync on|off|now|status"}' >&2; exit 1 ;;
172
+ esac
173
+ ;;
174
+ extend) # extend add <file.md> | rm <name> | list | gaps — the /network-extend protocol registry
175
+ shift; sub="${1:-list}"
176
+ if [[ "$sub" == "gaps" ]]; then exec "$0" needs; fi
177
+ exec "$0" protocol "$@"
178
+ ;;
179
+ gift) # gift <object-id16> <recipient-pubkey> — human-gated object transfer
180
+ shift; exec "$0" give "$@"
181
+ ;;
182
+ altkey) # altkey add|revoke <pubkey> | list — link your other device keys (desktop app) to your membership
183
+ shift; exec node "$PACK_DIR/bin/hive-net.mjs" altkey "$@"
184
+ ;;
185
+ admin) # admin invite — mint a member invite + join link (operator only)
186
+ shift; sub="${1:-}"
187
+ [[ "$sub" == "invite" ]] || { echo '{"error":"usage: hive admin invite [--server <url>]"}' >&2; exit 1; }
188
+ shift; exec node "$PACK_DIR/bin/hive-net.mjs" admin-invite "$@"
189
+ ;;
190
+ doctor) # doctor — identity, relay, wallet, gas, tokens, daemon, stores in one look
191
+ node -e '
192
+ const fs=require("fs"),os=require("os"),path=require("path");
193
+ const home=process.env.HIVE_HOME||path.join(os.homedir(),".hive");
194
+ const pack=process.argv[1];
195
+ const load=(p)=>{try{return JSON.parse(fs.readFileSync(p,"utf8"))}catch{return null}};
196
+ const out={};
197
+ const id=load(path.join(home,"identity.json"));
198
+ out.identity=id?{ok:true,pubkey:id.pubkey.slice(0,12)}:{ok:false,fix:"hive keygen"};
199
+ const cfg=load(path.join(home,"config.json"))||{};
200
+ out.relay={url:cfg.relay||"(unset)"};
201
+ const wal=load(path.join(home,"wallet.json"));
202
+ const rec=wal&&id?wal[id.pubkey]:null;
203
+ out.wallet=rec?{ok:true,evm:rec.evm_address}:{ok:false,fix:"hive wallet"};
204
+ let pid=null;try{pid=parseInt(fs.readFileSync(path.join(home,"daemon.pid"),"utf8"))}catch{}
205
+ let running=false;try{if(pid){process.kill(pid,0);running=true}}catch{}
206
+ out.local_daemon=running?{ok:true,pid}:{ok:false,note:"fine if your bee runs on the community server"};
207
+ out.bee=cfg.bee_name?{name:cfg.bee_name,server:cfg.server_url||null}:{note:"not provisioned — hive join"};
208
+ const cnt=(d)=>{try{return fs.readdirSync(path.join(home,d)).filter(f=>!f.startsWith(".")).length}catch{return 0}};
209
+ out.stores={data:cnt("data-store"),capability:cnt("capability-store"),object:cnt("object-store")};
210
+ (async()=>{
211
+ try{const r=await fetch((cfg.relay||"http://localhost:3000").replace(/^ws/,"http"),{signal:AbortSignal.timeout(4000)});out.relay.ok=r.status<500}catch{out.relay.ok=false;out.relay.fix="hive start (local) or hive connect <url>"}
212
+ const dep=load(path.join(pack,"onchain","deployments.sepolia.json"));
213
+ if(dep&&rec){try{
214
+ const {JsonRpcProvider,Contract,formatUnits,formatEther}=await import("ethers");
215
+ const p=new JsonRpcProvider(process.env.SEPOLIA_RPC_URL||"https://ethereum-sepolia-rpc.publicnode.com");
216
+ const bal=(a)=>new Contract(a,["function balanceOf(address) view returns (uint256)"],p).balanceOf(rec.evm_address);
217
+ const [h,j,g]=await Promise.all([bal(dep.honey),bal(dep.jelly),p.getBalance(rec.evm_address)]);
218
+ out.tokens={HONEY:formatUnits(h,18),JELLY:formatUnits(j,18),gas_eth:formatEther(g),contracts_v:dep.version||1};
219
+ }catch(e){out.tokens={ok:false,err:String(e.message).slice(0,80)}}}
220
+ console.log(JSON.stringify(out,null,2));
221
+ })();
222
+ ' "$PACK_DIR"
223
+ ;;
224
+ daemon) # daemon start|stop|status — one hived per machine, shared by all clients
225
+ shift; sub="${1:-status}"
226
+ PIDFILE="$HIVE_HOME/daemon.pid"
227
+ running() { [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; }
228
+ case "$sub" in
229
+ start)
230
+ if running; then echo "{\"daemon\":\"already-running\",\"pid\":$(cat "$PIDFILE")}"; exit 0; fi
231
+ nohup node "$PACK_DIR/bin/hived.mjs" >> "$HIVE_HOME/daemon.out" 2>&1 &
232
+ disown || true
233
+ sleep 1
234
+ if running; then echo "{\"daemon\":\"started\",\"pid\":$(cat "$PIDFILE")}"; else echo '{"daemon":"failed","log":"~/.hive/daemon.out"}' >&2; exit 1; fi
235
+ ;;
236
+ stop)
237
+ if running; then kill "$(cat "$PIDFILE")" && rm -f "$PIDFILE" && echo '{"daemon":"stopped"}'; else echo '{"daemon":"not-running"}'; fi
238
+ ;;
239
+ status)
240
+ if running; then echo "{\"daemon\":\"running\",\"pid\":$(cat "$PIDFILE")}"; else echo '{"daemon":"not-running"}'; fi
241
+ ;;
242
+ logs)
243
+ tail -20 "$HIVE_HOME/daemon.log" 2>/dev/null || echo "(no log yet)"
244
+ ;;
245
+ esac
246
+ ;;
247
+ give) # give <object-id16> <recipient-pubkey> — HUMAN-only object transfer.
248
+ # Deliberately not exposed to hived: the daemon has no spend authority
249
+ # (req 4). A transfer is a signed note in #hive-object-store naming the
250
+ # new owner; verifiers check the giver signed it and owned the object.
251
+ shift; OID="${1:-}"; TO="${2:-}"
252
+ [[ -n "$OID" && -n "$TO" ]] || { echo '{"error":"usage: hive give <object-id16> <recipient-pubkey>"}' >&2; exit 1; }
253
+ F="$HIVE_HOME/object-store/$OID.json"
254
+ [[ -f "$F" ]] || { echo '{"error":"object not found in your store"}' >&2; exit 1; }
255
+ read -r -p "Transfer object $OID to ${TO:0:12}...? [y/N] " ok
256
+ [[ "$ok" == "y" ]] || { echo '{"transfer":"cancelled"}'; exit 0; }
257
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
258
+ OBJ_CH="$("$0" ensure-channel hive-logs)"
259
+ node -e '
260
+ const fs=require("fs");const [f,to,pk]=process.argv.slice(1);
261
+ const o=JSON.parse(fs.readFileSync(f,"utf8"));
262
+ process.stdout.write(JSON.stringify({type:"hive-transfer",object:o.id,name:o.name,from:pk,to,at:Math.floor(Date.now()/1000)}));
263
+ ' "$F" "$TO" "$(json_field "$IDENTITY" pubkey)" | "$BUZZ" messages send --channel "$OBJ_CH" --content -
264
+ mkdir -p "$HIVE_HOME/object-store/given" && mv "$F" "$HIVE_HOME/object-store/given/"
265
+ ;;
266
+ feed) # your feed — results, tips, gifts, and settlements addressed to you (V1/V11)
267
+ # Member laptops have no buzz binary — the Node client covers them.
268
+ if [[ -z "$(find_buzz)" ]]; then exec node "$PACK_DIR/bin/hive-net.mjs" feed; fi
269
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
270
+ PK="$(json_field "$IDENTITY" pubkey)"
271
+ LOGS="$("$0" ensure-channel hive-logs)"
272
+ "$BUZZ" messages get --channel "$LOGS" --limit 400 | node -e '
273
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
274
+ const list=JSON.parse(d); const msgs=Array.isArray(list)?list:list.messages||[];
275
+ const me=process.argv[1];
276
+ const results=[],tips=[],gifts=[],settles=[]; const myOpen=new Set();
277
+ // pass 1: sessions I opened, so I can surface their settlements (V11)
278
+ for(const m of msgs){try{const j=JSON.parse(m.content); if(j.by&&j.by!==m.pubkey) continue;
279
+ if(j.type==="hive-session"&&m.pubkey===me&&typeof j.session_id==="string") myOpen.add(j.session_id);
280
+ }catch{}}
281
+ for(const m of msgs){try{const j=JSON.parse(m.content);
282
+ if(j.by&&j.by!==m.pubkey) continue; // R-B1 provenance
283
+ if(j.type==="hive-result"&&j.for===me) results.push({m,j});
284
+ else if(j.type==="hive-tip"&&j.to===me&&j.from===m.pubkey&&Number(j.amount)>0) tips.push({m,j});
285
+ else if(j.type==="hive-transfer"&&j.to===me) gifts.push({m,j});
286
+ else if(j.type==="hive-settle"&&myOpen.has(j.session_id)) settles.push({m,j});
287
+ }catch{}}
288
+ let any=false;
289
+ if(results.length){any=true;console.log("RESULTS:");for(const {m,j} of results)
290
+ 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");}
291
+ if(tips.length){any=true;console.log("TIPS RECEIVED:");for(const {j} of tips)
292
+ console.log(" • +"+j.amount+" copper from "+(j.from||"").slice(0,12)+(j.note?" — "+String(j.note).slice(0,80):""));}
293
+ if(gifts.length){any=true;console.log("GIFTS RECEIVED:");for(const {j} of gifts)
294
+ console.log(" • object "+String(j.object||j.name||"").slice(0,16)+" from "+(j.from||"").slice(0,12));}
295
+ if(settles.length){any=true;console.log("SETTLEMENTS (sessions you opened):");for(const {j} of settles)
296
+ console.log(" • ["+j.status+"] "+String(j.kind||"")+": "+String(j.result).replace(/\n/g," ").slice(0,200));}
297
+ if(!any)console.log("(feed empty — broadcast an intent in #hive-intents, tip/gift, or open a session)");
298
+ })' "$PK"
299
+ ;;
300
+ react) # react <result-event-id> up|down [note] — feedback on a result (V2; mints HONEY at the next epoch)
301
+ shift; RID="${1:-}"; DIR="${2:-up}"; NOTE="${3:-}"
302
+ [[ -n "$RID" ]] || { echo '{"error":"usage: hive react <result-event-id> up|down [note]"}' >&2; exit 1; }
303
+ [[ "$DIR" == "up" || "$DIR" == "down" ]] || { echo '{"error":"direction must be up or down"}' >&2; exit 1; }
304
+ if [[ -z "$(find_buzz)" ]]; then exec node "$PACK_DIR/bin/hive-net.mjs" react "$RID" "$DIR" "$NOTE"; fi
305
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
306
+ LOGS="$("$0" ensure-channel hive-logs)"
307
+ # Resolve the result's verified author (signer) so feedback is addressable.
308
+ RESULT_BY="$("$BUZZ" messages get --channel "$LOGS" --limit 200 | node -e '
309
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
310
+ const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];
311
+ const hit=a.find(m=>m.id===process.argv[1]);
312
+ if(!hit){console.log("");return;}
313
+ try{const j=JSON.parse(hit.content); console.log(j.by===hit.pubkey?hit.pubkey:"");}catch{console.log("")}
314
+ })' "$RID")"
315
+ [[ -n "$RESULT_BY" ]] || { echo '{"error":"result not found or unverifiable in hive-logs"}' >&2; exit 1; }
316
+ node -e '
317
+ const [rid,by,dir,note,me]=process.argv.slice(1);
318
+ process.stdout.write(JSON.stringify({type:"hive-feedback",result:rid,result_by:by,dir,note:note||undefined,by:me,at:Math.floor(Date.now()/1000)}));
319
+ ' "$RID" "$RESULT_BY" "$DIR" "$NOTE" "$(json_field "$IDENTITY" pubkey)" | "$BUZZ" messages send --channel "$LOGS" --content -
320
+ ;;
321
+ roi) # roi — what you've contributed vs received, and the highest-leverage thing to add (V3)
322
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
323
+ PK="$(json_field "$IDENTITY" pubkey)"
324
+ LOGS="$("$0" ensure-channel hive-logs)"
325
+ FB="$(cat "$HIVE_HOME/feedback.json" 2>/dev/null || echo '{}')"
326
+ "$BUZZ" messages get --channel "$LOGS" --limit 300 | node -e '
327
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
328
+ const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];
329
+ const me=process.argv[1]; const fb=JSON.parse(process.argv[2]||"{}");
330
+ const fs=require("fs"),os=require("os"),path=require("path");
331
+ const home=process.env.HIVE_HOME||path.join(os.homedir(),".hive");
332
+ let given=0,received=0,intents=0;
333
+ for(const m of a){try{const j=JSON.parse(m.content); if(j.by!==m.pubkey) continue;
334
+ if(j.type==="hive-result"&&j.by===me) given++;
335
+ if(j.type==="hive-result"&&j.for===me) received++;
336
+ if(j.type==="hive-intent"&&j.for===me) intents++;
337
+ }catch{}}
338
+ const cnt=(dir)=>{try{return fs.readdirSync(path.join(home,dir)).filter(f=>!f.startsWith(".")).length}catch{return 0}};
339
+ const data=cnt("data-store"),cap=cnt("capability-store"),obj=cnt("object-store");
340
+ // profile section richness → the ROI hint
341
+ let profile=""; try{profile=fs.readFileSync(path.join(home,"data-store","profile.md"),"utf8")}catch{}
342
+ const thin=[];
343
+ if(!/movie|film|watch/i.test(profile)) thin.push("movie/show tastes → unlocks group movie & watch recs");
344
+ if(!/book|read/i.test(profile)) thin.push("reading list → unlocks book/read-alike matches");
345
+ if(cap<3) thin.push("more capabilities (hive-capability-store) → more intents you can answer for others");
346
+ if(obj<3) thin.push("mint more objects (hive mint) → more to trade/tip with");
347
+ console.log("Hive ROI for "+me.slice(0,12));
348
+ console.log(" contributed (results you produced): "+given);
349
+ console.log(" received (results for you): "+received);
350
+ console.log(" intents raised for you: "+intents);
351
+ console.log(" reputation from reactions: +"+(fb.up||0)+" / -"+(fb.down||0));
352
+ console.log(" stores: data="+data+" capability="+cap+" object="+obj);
353
+ console.log("\nAdd this next (highest leverage):");
354
+ if(thin.length){console.log(" → "+thin[0]);} else {console.log(" → your stores are well-stocked; raise more intents in #hive-intents");}
355
+ })' "$PK" "$FB"
356
+ ;;
357
+ verify-stores) # verify-stores — hard gate: all three stores non-empty (J9)
358
+ node -e '
359
+ const fs=require("fs"),os=require("os"),path=require("path");
360
+ const home=process.env.HIVE_HOME||path.join(os.homedir(),".hive");
361
+ const nonempty=(d)=>{try{return fs.readdirSync(path.join(home,d)).some(f=>{
362
+ if(f.startsWith(".")) return false; const p=path.join(home,d,f);
363
+ const st=fs.statSync(p); return st.isDirectory()?fs.readdirSync(p).length>0:st.size>0;
364
+ })}catch{return false}};
365
+ const empty=["data-store","capability-store","object-store"].filter(d=>!nonempty(d));
366
+ if(empty.length){console.error(JSON.stringify({ok:false,empty}));process.exit(1);}
367
+ console.log(JSON.stringify({ok:true,stores:["data-store","capability-store","object-store"]}));
368
+ '
369
+ ;;
370
+ protocol) # protocol add <file.md> | rm <name> | list — the registry every daemon auto-uses
371
+ shift; sub="${1:-list}"
372
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
373
+ case "$sub" in
374
+ add)
375
+ f="${2:-}"
376
+ [[ -f "$f" ]] || { echo '{"error":"protocol file not found"}' >&2; exit 1; }
377
+ name="$(awk '/^name:/{sub(/^name: */,"");print;exit}' "$f")"
378
+ match="$(awk '/^match:/{sub(/^match: */,"");print;exit}' "$f")"
379
+ [[ -n "$name" && -n "$match" ]] || { echo '{"error":"protocol file needs frontmatter lines: name: <n> and match: <kw,kw>"}' >&2; exit 1; }
380
+ [[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,63}$ ]] || { echo '{"error":"protocol name must be a slug: lowercase a-z 0-9 and dashes, starting alphanumeric"}' >&2; exit 1; }
381
+ # P7 (validate: length, body starts at first heading) + R-C2 (reject
382
+ # shell/exfil/transfer bodies) BEFORE any network call. On failure the
383
+ # payload is empty and nothing is published.
384
+ payload="$(node -e '
385
+ const fs=require("fs");
386
+ const [f,name,match,pk]=process.argv.slice(1);
387
+ const raw=fs.readFileSync(f,"utf8");
388
+ const h=raw.indexOf("\n#"); const body=(h>=0?raw.slice(h+1):raw).trim();
389
+ if(!body){console.error(JSON.stringify({error:"protocol body empty (need a # heading + numbered steps)"}));process.exit(1);}
390
+ if(body.length>6000){console.error(JSON.stringify({error:"protocol body too long: "+body.length+" > 6000 chars"}));process.exit(1);}
391
+ const danger=(b)=>{
392
+ if(/\$\(|\brm\s+-rf\b|\b(?:curl|wget)\s+(?:-|https?:\/\/)|\b(?:bash|sh|zsh|python3?|node)\s+\S+\.\w|\beval\s*\(|\bexec\s*\(|\|\s*(?:sh|bash|zsh)\b/i.test(b))return "shell/command-execution";
393
+ if(/\b(privkey|private key|secret key|mnemonic|seed phrase|nsec1|identity\.json)\b/i.test(b))return "secret-exfiltration";
394
+ if(/\b(transfer|send|move|drain|withdraw)\b[^.\n]{0,30}\b(funds?|money|copper|balance|wallet|tokens?)\b/i.test(b))return "value-transfer";
395
+ return null;};
396
+ const dg=danger(body);
397
+ if(dg){console.error(JSON.stringify({error:"protocol rejected ("+dg+"): bodies are FOLLOW-guidance treated as untrusted data"}));process.exit(1);}
398
+ process.stdout.write(JSON.stringify({type:"hive-protocol",name,match,body,by:pk}));
399
+ ' "$f" "$name" "$match" "$(json_field "$IDENTITY" pubkey)")" || exit 1
400
+ PROT_ID="$("$0" ensure-channel hive-logs)" # network only AFTER validation passes
401
+ printf '%s' "$payload" | "$BUZZ" messages send --channel "$PROT_ID" --content -
402
+ ;;
403
+ rm) # rm <name> — publish an owner-signed tombstone; daemons drop it (P11)
404
+ name="${2:-}"
405
+ [[ -n "$name" ]] || { echo '{"error":"usage: hive protocol rm <name>"}' >&2; exit 1; }
406
+ PROT_ID="$("$0" ensure-channel hive-logs)"
407
+ node -e '
408
+ const [name,pk]=process.argv.slice(1);
409
+ process.stdout.write(JSON.stringify({type:"hive-protocol",name,tombstone:true,by:pk}));
410
+ ' "$name" "$(json_field "$IDENTITY" pubkey)" | "$BUZZ" messages send --channel "$PROT_ID" --content -
411
+ ;;
412
+ list)
413
+ PROT_ID="$("$0" ensure-channel hive-logs)"
414
+ "$BUZZ" messages get --channel "$PROT_ID" --limit 200 | node -e '
415
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
416
+ const list=JSON.parse(d); const msgs=Array.isArray(list)?list:list.messages||[];
417
+ // Mirror the daemon (syncProtocols): validate the name, fold
418
+ // chronologically with tombstone-before-add tie-break, first-author
419
+ // ownership survives tombstones (dead entry retains by), hide
420
+ // dangerous bodies. Diverging from the daemon here is a real bug.
421
+ const valid=(n)=>typeof n==="string"&&/^[a-z0-9][a-z0-9-]{0,63}$/.test(n);
422
+ const danger=(b)=>/\$\(|\brm\s+-rf\b|\b(?:curl|wget)\s+(?:-|https?:\/\/)|\b(?:bash|sh|zsh|python3?|node)\s+\S+\.\w|\beval\s*\(|\bexec\s*\(|\|\s*(?:sh|bash|zsh)\b/i.test(b)
423
+ ||/\b(privkey|private key|secret key|mnemonic|seed phrase|nsec1|identity\.json)\b/i.test(b)
424
+ ||/\b(transfer|send|move|drain|withdraw)\b[^.\n]{0,30}\b(funds?|money|copper|balance|wallet|tokens?)\b/i.test(b);
425
+ const items=[];
426
+ for(const m of msgs){try{const p=JSON.parse(m.content);
427
+ if(!p||p.type!=="hive-protocol"||!valid(p.name)) continue;
428
+ if(p.match!=null&&typeof p.match!=="string") continue;
429
+ if(!p.tombstone&&typeof p.body!=="string") continue;
430
+ items.push({at:m.created_at||0,by:m.pubkey,p});
431
+ }catch{}}
432
+ items.sort((a,b)=>(a.at-b.at)||((b.p.tombstone?1:0)-(a.p.tombstone?1:0)));
433
+ const reg=Object.create(null);
434
+ for(const {at,by,p} of items){ const cur=reg[p.name];
435
+ if(cur&&cur.by&&cur.by!==by) continue; // takeover rejected
436
+ if(p.tombstone){ if(cur&&at>=cur.at&&!cur.dead) reg[p.name]={name:p.name,by:cur.by,at,dead:true}; continue; }
437
+ if(danger(String(p.body))) continue;
438
+ if(!cur||at>=cur.at) reg[p.name]={name:p.name,match:p.match,by,at,dead:false};
439
+ }
440
+ for(const p of Object.values(reg)) if(!p.dead) console.log(JSON.stringify({name:p.name,match:p.match,by:(p.by||"").slice(0,12)}));
441
+ })'
442
+ ;;
443
+ *) echo '{"error":"usage: hive protocol add <file.md> | rm <name> | list"}' >&2; exit 1 ;;
444
+ esac
445
+ ;;
446
+ tip) # tip <amount> <member-pubkey|0x-addr> — send $JELLY on Sepolia (HUMAN-gated; never from hived)
447
+ shift; AMT="${1:-}"; TO="${2:-}"
448
+ [[ "$AMT" =~ ^[0-9]+(\.[0-9]+)?$ && -n "$TO" ]] || { echo '{"error":"usage: hive tip <amount> <member-pubkey|0x-address>"}' >&2; exit 1; }
449
+ JELLY="$(token_addr jelly)"; [[ -n "$JELLY" ]] || { echo '{"error":"$JELLY not deployed yet — run onchain/deploy.sh"}' >&2; exit 1; }
450
+ DEST="$(resolve_evm "$TO")"; [[ "$DEST" =~ ^0x[0-9a-fA-F]{40}$ ]] || { echo "{\"error\":\"no announced EVM address for ${TO:0:12} (they must run: hive wallet)\"}" >&2; exit 1; }
451
+ WEI="$("$CAST" to-wei "$AMT" ether)"
452
+ read -r -p "Send $AMT JELLY to $DEST on Sepolia? [y/N] " ok
453
+ [[ "$ok" == "y" ]] || { echo '{"tip":"cancelled"}'; exit 0; }
454
+ KEY="$(signer_key)"; [[ -n "$KEY" ]] || { echo '{"error":"no signing key in Keychain"}' >&2; exit 1; }
455
+ TX="$("$CAST" send "$JELLY" "transfer(address,uint256)" "$DEST" "$WEI" --private-key "$KEY" --rpc-url "$SEPOLIA_RPC" --json 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).transactionHash)}catch{console.log('')}})")"
456
+ unset KEY
457
+ [[ -n "$TX" ]] && echo "{\"tipped\":\"$AMT JELLY\",\"to\":\"$DEST\",\"tx\":\"$TX\",\"explorer\":\"https://sepolia.etherscan.io/tx/$TX\"}" || { echo '{"error":"transfer failed — is the wallet funded for gas and holding JELLY?"}' >&2; exit 1; }
458
+ ;;
459
+ pay) # pay "<english>" — natural-language tip, e.g. hive pay "tip siddharth 1 $JELLY" (HUMAN-gated on-chain send)
460
+ shift; PHRASE="$*"
461
+ [[ -n "$PHRASE" ]] || { echo '{"error":"usage: hive pay \"tip <name> <amount> $JELLY|$HONEY\""}' >&2; exit 1; }
462
+ # Parse: "tip NAME AMT [$]TOKEN" or "send AMT [$]TOKEN to NAME". Token defaults to JELLY.
463
+ PARSED="$(node -e '
464
+ const s=process.argv[1];
465
+ let m=s.match(/(?:tip|pay|send|give)\s+([a-z0-9_.-]{2,})\s+([0-9]+(?:\.[0-9]+)?)\s*\$?\s*(honey|jelly)?\b/i)
466
+ || s.match(/(?:tip|pay|send|give)\s+([0-9]+(?:\.[0-9]+)?)\s*\$?\s*(honey|jelly)?\s+to\s+([a-z0-9_.-]{2,})/i);
467
+ if(!m){process.stdout.write("ERR could not parse — try: tip <name> <amount> $JELLY");process.exit(0);}
468
+ let name,amount,token;
469
+ if(/^[0-9]/.test(m[1])){amount=m[1];token=m[2];name=m[3];} else {name=m[1];amount=m[2];token=m[3];}
470
+ process.stdout.write(name+" "+amount+" "+(token||"jelly").toLowerCase());
471
+ ' "$PHRASE")"
472
+ [[ "$PARSED" == ERR* ]] && { echo "{\"error\":\"${PARSED#ERR }\"}" >&2; exit 1; }
473
+ read -r NAME AMT TOK <<< "$PARSED"
474
+ TOKADDR="$(token_addr "$TOK")"; [[ -n "$TOKADDR" ]] || { echo "{\"error\":\"\$$(echo "$TOK"|tr a-z A-Z) not deployed — run onchain/deploy.sh\"}" >&2; exit 1; }
475
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
476
+ # Resolve NAME -> member pubkey (buzz display-name search); accept a raw 64-hex pubkey too.
477
+ RES=""
478
+ if [[ "$NAME" =~ ^[0-9a-fA-F]{64}$ ]]; then RES="$NAME"; else
479
+ RES="$("$BUZZ" users get --name "$NAME" 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const l=JSON.parse(d);const a=Array.isArray(l)?l:(l.users||[l]);const u=a[0];process.stdout.write((u&&(u.pubkey||u.id||u.author))||'')}catch{}})")"
480
+ fi
481
+ [[ -n "$RES" ]] || { echo "{\"error\":\"no member matching '$NAME'\"}" >&2; exit 1; }
482
+ DEST="$(resolve_evm "$RES")"; [[ "$DEST" =~ ^0x[0-9a-fA-F]{40}$ ]] || { echo "{\"error\":\"'$NAME' has no announced wallet (they must run: hive wallet)\"}" >&2; exit 1; }
483
+ WEI="$("$CAST" to-wei "$AMT" ether)"
484
+ read -r -p "Send $AMT $(echo "$TOK"|tr a-z A-Z) to $NAME ($DEST) on Sepolia? [y/N] " ok
485
+ [[ "$ok" == "y" ]] || { echo '{"pay":"cancelled"}'; exit 0; }
486
+ KEY="$(signer_key)"; TX="$("$CAST" send "$TOKADDR" "transfer(address,uint256)" "$DEST" "$WEI" --private-key "$KEY" --rpc-url "$SEPOLIA_RPC" --json 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).transactionHash)}catch{console.log('')}})")"; unset KEY
487
+ [[ -n "$TX" ]] && echo "{\"paid\":\"$AMT $(echo "$TOK"|tr a-z A-Z)\",\"to\":\"$NAME\",\"address\":\"$DEST\",\"tx\":\"$TX\",\"explorer\":\"https://sepolia.etherscan.io/tx/$TX\"}" || { echo '{"error":"send failed — gas? token balance?"}' >&2; exit 1; }
488
+ ;;
489
+ balance) # balance [addr] — on-chain $HONEY + $JELLY balances (Sepolia)
490
+ shift; ADDR="${1:-$(my_evm)}"
491
+ [[ "$ADDR" =~ ^0x[0-9a-fA-F]{40}$ ]] || { echo '{"error":"no EVM address (run: hive wallet)"}' >&2; exit 1; }
492
+ HONEY="$(token_addr honey)"; JELLY="$(token_addr jelly)"
493
+ [[ -n "$HONEY" || -n "$JELLY" ]] || { echo '{"error":"tokens not deployed yet — run onchain/deploy.sh once the wallet has Sepolia ETH"}' >&2; exit 1; }
494
+ bal() { [[ -n "$1" ]] || { echo "n/a"; return; }; local w; w="$("$CAST" call "$1" "balanceOf(address)(uint256)" "$ADDR" --rpc-url "$SEPOLIA_RPC" 2>/dev/null | awk '{print $1}')"; [[ "$w" =~ ^[0-9]+$ ]] && "$CAST" to-unit "$w" ether || echo "err"; }
495
+ echo "{\"address\":\"$ADDR\",\"HONEY\":\"$(bal "$HONEY")\",\"JELLY\":\"$(bal "$JELLY")\"}"
496
+ ;;
497
+ honey|jelly) # honey|jelly balance [addr] | mint <to> <amount> — token ops (mint is owner-only, HUMAN-gated)
498
+ TOK="$cmd"; TU="$(echo "$TOK" | tr a-z A-Z)"; shift; sub="${1:-balance}"
499
+ ADDR_TOKEN="$(token_addr "$TOK")"; [[ -n "$ADDR_TOKEN" ]] || { echo "{\"error\":\"\$$TU not deployed — run onchain/deploy.sh\"}" >&2; exit 1; }
500
+ case "$sub" in
501
+ balance) shift; A="${1:-$(my_evm)}"; [[ "$A" =~ ^0x[0-9a-fA-F]{40}$ ]] || { echo '{"error":"bad address"}' >&2; exit 1; }
502
+ W="$("$CAST" call "$ADDR_TOKEN" "balanceOf(address)(uint256)" "$A" --rpc-url "$SEPOLIA_RPC" 2>/dev/null | awk '{print $1}')"
503
+ echo "{\"token\":\"$TU\",\"address\":\"$A\",\"balance\":\"$("$CAST" to-unit "${W:-0}" ether)\"}" ;;
504
+ mint) shift; TO="${1:-}"; AMT="${2:-}"; [[ -n "$TO" && "$AMT" =~ ^[0-9]+(\.[0-9]+)?$ ]] || { echo "{\"error\":\"usage: hive $TOK mint <to-addr|pubkey> <amount>\"}" >&2; exit 1; }
505
+ DEST="$(resolve_evm "$TO")"; [[ "$DEST" =~ ^0x[0-9a-fA-F]{40}$ ]] || { echo '{"error":"could not resolve destination EVM address"}' >&2; exit 1; }
506
+ WEI="$("$CAST" to-wei "$AMT" ether)"
507
+ read -r -p "Mint $AMT $TU to $DEST? (owner-only) [y/N] " ok; [[ "$ok" == "y" ]] || { echo '{"mint":"cancelled"}'; exit 0; }
508
+ KEY="$(signer_key)"; TX="$("$CAST" send "$ADDR_TOKEN" "mint(address,uint256)" "$DEST" "$WEI" --private-key "$KEY" --rpc-url "$SEPOLIA_RPC" --json 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).transactionHash)}catch{console.log('')}})")"; unset KEY
509
+ [[ -n "$TX" ]] && echo "{\"minted\":\"$AMT $TU\",\"to\":\"$DEST\",\"tx\":\"$TX\"}" || { echo '{"error":"mint failed — are you the owner and funded for gas?"}' >&2; exit 1; } ;;
510
+ *) echo "{\"error\":\"usage: hive $TOK balance|mint\"}" >&2; exit 1 ;;
511
+ esac
512
+ ;;
513
+ gov) # gov propose "<text>" | vote <id> yes|no | tally <id> — HONEY-weighted, snapshot block + 48h deadline + 30% quorum
514
+ shift; exec node "$PACK_DIR/bin/hive-net.mjs" gov "$@"
515
+ ;;
516
+ bounty) # bounty "<task>" --pool N [--deadline s] — winner-take-all JELLY session
517
+ shift; TASK=""; POOL=""; DL=900
518
+ while [[ $# -gt 0 ]]; do case "$1" in
519
+ --pool) POOL="${2:-}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
520
+ --deadline) DL="${2:-900}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
521
+ *) TASK="$1"; shift ;;
522
+ esac; done
523
+ [[ -n "$TASK" && "$POOL" =~ ^[0-9]+(\.[0-9]+)?$ ]] || { echo '{"error":"usage: hive bounty \"<task>\" --pool <jelly> [--deadline secs]"}' >&2; exit 1; }
524
+ exec "$0" session open --kind bounty --pool "$POOL" --payout winner --deadline "$DL" --quorum 1 "$TASK"
525
+ ;;
526
+ order) # order "<craving>" [--quorum n] [--deadline s] — group food ordering session
527
+ shift; ASK=""; Q=2; DL=900
528
+ while [[ $# -gt 0 ]]; do case "$1" in
529
+ --quorum) Q="${2:-2}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
530
+ --deadline) DL="${2:-900}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
531
+ *) ASK="$1"; shift ;;
532
+ esac; done
533
+ [[ -n "$ASK" ]] || { echo '{"error":"usage: hive order \"im hungry — thai?\" [--quorum n]"}' >&2; exit 1; }
534
+ exec "$0" session open --kind food-order --deadline "$DL" --quorum "$Q" "$ASK"
535
+ ;;
536
+ predict) # predict "<question>" --resolver <name|pubkey> [--stake N] — private prediction market (human resolver, never the subject's own bee)
537
+ shift; Qs=""; RESOLVER=""; STAKE=""; DL=86400
538
+ while [[ $# -gt 0 ]]; do case "$1" in
539
+ --resolver) RESOLVER="${2:-}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
540
+ --stake) STAKE="${2:-}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
541
+ --deadline) DL="${2:-86400}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
542
+ *) Qs="$1"; shift ;;
543
+ esac; done
544
+ [[ -n "$Qs" && -n "$RESOLVER" ]] || { echo '{"error":"usage: hive predict \"<question>\" --resolver <member> [--stake N] — the resolver must be a named human, not the opener"}' >&2; exit 1; }
545
+ RPK="$(node "$PACK_DIR/bin/hive-net.mjs" resolve-user "$RESOLVER")" || exit 1
546
+ [[ "$RPK" != "$(json_field "$IDENTITY" pubkey)" ]] || { echo '{"error":"the resolver cannot be the opener — pick a neutral member"}' >&2; exit 1; }
547
+ POOLARGS=()
548
+ [[ -n "$STAKE" ]] && POOLARGS=(--pool "$STAKE" --payout winner)
549
+ exec "$0" session open --kind predict --resolver "$RPK" --deadline "$DL" --quorum 2 "${POOLARGS[@]}" "$Qs"
550
+ ;;
551
+ dnd) # dnd on|off [--price N] — pay-to-interrupt (the fee is YOUR price, 100% to you)
552
+ shift; exec node "$PACK_DIR/bin/hive-net.mjs" dnd "$@"
553
+ ;;
554
+ directory) # directory — endpoints seen on the network, newest activity first (J10)
555
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
556
+ LOGS="$("$0" ensure-channel hive-logs)"
557
+ "$BUZZ" messages get --channel "$LOGS" --limit 400 | node -e '
558
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
559
+ const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];
560
+ const ep={};
561
+ for(const m of a){try{const j=JSON.parse(m.content); if(!m.pubkey) continue;
562
+ if(j.by && j.by!==m.pubkey) continue; // provenance
563
+ const e=ep[m.pubkey]||(ep[m.pubkey]={pubkey:m.pubkey,at:0});
564
+ const at=m.created_at||0; if(at>e.at) e.at=at;
565
+ if(j.type==="hive-wallet"){ e.evm=j.evm; e.solana=j.solana; }
566
+ }catch{}}
567
+ const rows=Object.values(ep).sort((x,y)=>y.at-x.at);
568
+ if(!rows.length){console.log("(no endpoints seen yet)");return;}
569
+ for(const e of rows) console.log("• "+e.pubkey.slice(0,12)+(e.evm?" evm:"+String(e.evm).slice(0,10)+"…":"")+(e.solana?" sol:"+String(e.solana).slice(0,8)+"…":""));
570
+ })'
571
+ ;;
572
+ needs) # needs — unmet intents the network could not serve, i.e. protocol gaps (V6)
573
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
574
+ LOGS="$("$0" ensure-channel hive-logs)"
575
+ "$BUZZ" messages get --channel "$LOGS" --limit 400 | node -e '
576
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
577
+ const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];
578
+ const cnt={};
579
+ for(const m of a){try{const j=JSON.parse(m.content);
580
+ if(j.type!=="hive-need"||(j.by&&j.by!==m.pubkey)) continue;
581
+ const k=(j.intent||"").toLowerCase().slice(0,80); if(!k) continue;
582
+ cnt[k]=(cnt[k]||0)+1;
583
+ }catch{}}
584
+ const rows=Object.entries(cnt).sort((x,y)=>y[1]-x[1]);
585
+ if(!rows.length){console.log("(no unmet needs recorded — the network is answering what it is asked)");return;}
586
+ console.log("Top network gaps (write a protocol to fill one — see hive-protocol-author):");
587
+ for(const [k,n] of rows.slice(0,15)) console.log(" "+n+"× "+k);
588
+ })'
589
+ ;;
590
+ block|unblock) # block <pubkey> | unblock <pubkey> — local blocklist the daemon honors (R-E1)
591
+ PK2="${2:-}"; [[ -n "$PK2" ]] || { echo "{\"error\":\"usage: hive $cmd <pubkey>\"}" >&2; exit 1; }
592
+ node -e '
593
+ const fs=require("fs");const [path,op,pk]=process.argv.slice(1);
594
+ let b=[];try{b=JSON.parse(fs.readFileSync(path,"utf8"))}catch{}; if(!Array.isArray(b))b=[];
595
+ if(op==="block"){ if(!b.includes(pk)) b.push(pk); } else { b=b.filter(x=>x!==pk); }
596
+ fs.writeFileSync(path,JSON.stringify(b));
597
+ console.log(JSON.stringify({[op+"ed"]:pk.slice(0,12),blocked_total:b.length,note:"daemon applies within one poll"}));
598
+ ' "$HIVE_HOME/blocklist.json" "$cmd" "$PK2"
599
+ ;;
600
+ blocks) # blocks — list locally blocked pubkeys
601
+ node -e 'const fs=require("fs");let b=[];try{b=JSON.parse(fs.readFileSync(process.argv[1],"utf8"))}catch{};console.log(JSON.stringify(Array.isArray(b)?b:[]))' "$HIVE_HOME/blocklist.json"
602
+ ;;
603
+ report) # report <pubkey> [reason] — block locally AND broadcast a shareable hive-report (R-E1)
604
+ PK2="${2:-}"; REASON="${3:-}"; [[ -n "$PK2" ]] || { echo '{"error":"usage: hive report <pubkey> [reason]"}' >&2; exit 1; }
605
+ "$0" block "$PK2" >/dev/null
606
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
607
+ LOGS="$("$0" ensure-channel hive-logs)"; PK="$(json_field "$IDENTITY" pubkey)"
608
+ node -e '
609
+ const [subject,reason,pk]=process.argv.slice(1);
610
+ process.stdout.write(JSON.stringify({type:"hive-report",subject,reason:reason||undefined,by:pk,at:Math.floor(Date.now()/1000)}));
611
+ ' "$PK2" "$REASON" "$PK" | "$BUZZ" messages send --channel "$LOGS" --content -
612
+ ;;
613
+ session) # session open --kind K [--deadline s] [--quorum n] "prompt" | show <id> — orchestrator (V7)
614
+ shift; sub="${1:-show}"
615
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
616
+ case "$sub" in
617
+ open)
618
+ shift; KIND=""; DEADLINE=300; QUORUM=1; PROMPT=""; POOL=0; PAYOUT=""; RESOLVER=""
619
+ while [[ $# -gt 0 ]]; do case "$1" in
620
+ --kind) KIND="${2:-}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
621
+ --deadline) DEADLINE="${2:-300}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
622
+ --quorum) QUORUM="${2:-1}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
623
+ --pool) POOL="${2:-0}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
624
+ --payout) PAYOUT="${2:-}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
625
+ --resolver) RESOLVER="${2:-}"; if [[ $# -ge 2 ]]; then shift 2; else shift; fi ;;
626
+ *) PROMPT="$1"; shift ;;
627
+ esac; done
628
+ [[ -n "$KIND" && -n "$PROMPT" ]] || { echo '{"error":"usage: hive session open --kind <k> [--deadline secs] [--quorum n] [--pool <jelly> --payout split|winner] \"prompt\""}' >&2; exit 1; }
629
+ [[ "$DEADLINE" =~ ^[0-9]+$ && "$QUORUM" =~ ^[0-9]+$ ]] || { echo '{"error":"--deadline and --quorum must be integers"}' >&2; exit 1; }
630
+ [[ "$POOL" =~ ^[0-9]+(\.[0-9]+)?$ ]] || { echo '{"error":"--pool must be a number ($JELLY)"}' >&2; exit 1; }
631
+ if [[ "$POOL" != "0" ]]; then [[ "$PAYOUT" == "split" || "$PAYOUT" == "winner" ]] || { echo '{"error":"a --pool needs --payout split|winner"}' >&2; exit 1; }; fi
632
+ LOGS="$("$0" ensure-channel hive-logs)"; PK="$(json_field "$IDENTITY" pubkey)"
633
+ SIDF="$(mktemp)"
634
+ SIDF="$SIDF" node -e '
635
+ const [kind,deadline,quorum,prompt,pk,pool,payout,resolver]=process.argv.slice(1);
636
+ const sid=require("crypto").randomUUID();
637
+ const dl=Math.floor(Date.now()/1000)+parseInt(deadline,10);
638
+ require("fs").writeFileSync(process.env.SIDF, sid);
639
+ const ev={type:"hive-session",session_id:sid,kind,prompt:prompt.slice(0,500),deadline:dl,quorum:parseInt(quorum,10),resolver:(/^[0-9a-f]{64}$/i.test(resolver||"")?resolver:pk),by:pk};
640
+ if(Number(pool)>0){ev.pool=Number(pool);ev.payout_mode=payout;}
641
+ process.stdout.write(JSON.stringify(ev));
642
+ ' "$KIND" "$DEADLINE" "$QUORUM" "$PROMPT" "$PK" "$POOL" "$PAYOUT" "$RESOLVER" | "$BUZZ" messages send --channel "$LOGS" --content - >/dev/null
643
+ SID="$(cat "$SIDF")"; rm -f "$SIDF"
644
+ POOLNOTE=""; [[ "$POOL" != "0" ]] && POOLNOTE=",\"pool\":\"$POOL JELLY\",\"payout\":\"$PAYOUT\""
645
+ echo "{\"session\":\"$SID\",\"kind\":\"$KIND\",\"deadline_secs\":$DEADLINE,\"quorum\":$QUORUM$POOLNOTE,\"note\":\"resolves automatically at deadline\"}"
646
+ ;;
647
+ payout) # payout <session-id> — opener executes the settlement's proposed $JELLY payout (HUMAN-gated)
648
+ SID="${2:-}"; [[ -n "$SID" ]] || { echo '{"error":"usage: hive session payout <session-id>"}' >&2; exit 1; }
649
+ JELLY="$(token_addr jelly)"; [[ -n "$JELLY" ]] || { echo '{"error":"$JELLY not deployed"}' >&2; exit 1; }
650
+ LOGS="$("$0" ensure-channel hive-logs)"; ME="$(json_field "$IDENTITY" pubkey)"
651
+ # pull the settlement's payout array (opener-only; provenance-checked)
652
+ PAYJSON="$("$BUZZ" messages get --channel "$LOGS" --limit 400 | node -e '
653
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];const sid=process.argv[1],me=process.argv[2];
654
+ let opener="",settle=null;
655
+ for(const m of a){try{const j=JSON.parse(m.content); if(j.by&&j.by!==m.pubkey) continue; if(j.session_id!==sid) continue;
656
+ if(j.type==="hive-session") opener=m.pubkey;
657
+ else if(j.type==="hive-settle") settle=j;
658
+ }catch{}}
659
+ if(opener!==me){process.stdout.write("ERR only the session opener can pay out");return;}
660
+ if(!settle){process.stdout.write("ERR session not settled yet");return;}
661
+ if(!Array.isArray(settle.payout)||!settle.payout.length){process.stdout.write("ERR no payout proposed for this session");return;}
662
+ process.stdout.write(JSON.stringify(settle.payout));
663
+ })' "$SID" "$ME")"
664
+ [[ "$PAYJSON" == ERR* ]] && { echo "{\"error\":\"${PAYJSON#ERR }\"}" >&2; exit 1; }
665
+ echo "Proposed payout:"; echo "$PAYJSON" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{for(const p of JSON.parse(d))console.log(' '+p.jelly+' JELLY -> '+String(p.to).slice(0,12))})"
666
+ read -r -p "Execute these $JELLY transfers on Sepolia? [y/N] " ok
667
+ [[ "$ok" == "y" ]] || { echo '{"payout":"cancelled"}'; exit 0; }
668
+ KEY="$(signer_key)"; [[ -n "$KEY" ]] || { echo '{"error":"no signing key"}' >&2; exit 1; }
669
+ echo "$PAYJSON" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>process.stdout.write(JSON.parse(d).map(p=>p.to+' '+p.jelly).join('\n')+'\n'))" | while read -r TO AMT; do
670
+ DEST="$(resolve_evm "$TO")"
671
+ if [[ "$DEST" =~ ^0x[0-9a-fA-F]{40}$ ]]; then
672
+ WEI="$("$CAST" to-wei "$AMT" ether)"
673
+ TX="$("$CAST" send "$JELLY" "transfer(address,uint256)" "$DEST" "$WEI" --private-key "$KEY" --rpc-url "$SEPOLIA_RPC" --json 2>/dev/null | node -e "let x='';process.stdin.on('data',c=>x+=c).on('end',()=>{try{console.log(JSON.parse(x).transactionHash)}catch{console.log('')}})")"
674
+ echo " paid $AMT JELLY -> ${TO:0:12} ($([ -n "$TX" ] && echo "$TX" || echo FAILED))"
675
+ else echo " skipped ${TO:0:12} (no announced wallet)"; fi
676
+ done
677
+ unset KEY
678
+ echo '{"payout":"done"}'
679
+ ;;
680
+ show)
681
+ SID="${2:-}"
682
+ [[ -n "$SID" ]] || { echo '{"error":"usage: hive session show <session-id>"}' >&2; exit 1; }
683
+ LOGS="$("$0" ensure-channel hive-logs)"
684
+ "$BUZZ" messages get --channel "$LOGS" --limit 400 | node -e '
685
+ let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
686
+ const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];const sid=process.argv[1];
687
+ let sess=null; const offers={}; let settle=null;
688
+ for(const m of a){try{const j=JSON.parse(m.content); if(j.by&&j.by!==m.pubkey) continue; if(j.session_id!==sid) continue;
689
+ if(j.type==="hive-session") sess={kind:j.kind,opener:m.pubkey,prompt:j.prompt,deadline:j.deadline,quorum:j.quorum,pool:j.pool,payout_mode:j.payout_mode};
690
+ else if(j.type==="hive-offer"&&typeof j.offer==="string") offers[m.pubkey]=j.offer;
691
+ else if(j.type==="hive-settle") settle={status:j.status,result:j.result,offers:j.offers,pool:j.pool,payout:j.payout};
692
+ }catch{}}
693
+ if(!sess){console.log("(session not found in recent window)");return;}
694
+ console.log("Session "+sid.slice(0,8)+" kind="+sess.kind+" quorum="+sess.quorum+(sess.pool?(" pool="+sess.pool+" JELLY ("+sess.payout_mode+")"):""));
695
+ console.log(" ask: "+sess.prompt);
696
+ const now=Math.floor(Date.now()/1000);
697
+ console.log(" deadline: "+(sess.deadline?(now<sess.deadline?("in "+(sess.deadline-now)+"s"):"passed"):"none"));
698
+ console.log(" offers ("+Object.keys(offers).length+"):");
699
+ for(const [pk,o] of Object.entries(offers)) console.log(" • "+pk.slice(0,8)+": "+String(o).replace(/\n/g," ").slice(0,120));
700
+ if(settle){console.log(" SETTLED ["+settle.status+"]: "+String(settle.result).replace(/\n/g," ").slice(0,400));
701
+ if(Array.isArray(settle.payout)&&settle.payout.length){console.log(" PROPOSED PAYOUT (run: hive session payout "+sid.slice(0,8)+"…):");for(const p of settle.payout)console.log(" "+p.jelly+" JELLY -> "+String(p.to).slice(0,12));}}
702
+ else console.log(" (not settled yet)");
703
+ })' "$SID"
704
+ ;;
705
+ *) echo '{"error":"usage: hive session open|show"}' >&2; exit 1 ;;
706
+ esac
707
+ ;;
708
+ mem-ls)
709
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
710
+ exec "$BUZZ" mem ls --owner "$(json_field "$IDENTITY" owner_pubkey)"
711
+ ;;
712
+ mint)
713
+ shift; exec node "$PACK_DIR/bin/hive-mint.mjs" "$@"
714
+ ;;
715
+ claim-invite) # claim-invite <code|url> [--age-confirmed] — redeem a community invite with THIS key
716
+ shift; exec node "$PACK_DIR/bin/hive-claim-invite.mjs" "$@"
717
+ ;;
718
+ wallet) # wallet [show|export] [--agent <pubkey>] — EVM + Solana wallet; mnemonic in macOS Keychain
719
+ shift; node "$PACK_DIR/bin/hive-wallet.mjs" "$@"
720
+ # On create/refresh, announce PUBLIC addresses to the directory.
721
+ if [[ "${1:-create}" != "export" ]]; then
722
+ BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
723
+ PK="$(json_field "$IDENTITY" pubkey)"
724
+ DIR="$("$0" ensure-channel hive-logs)"
725
+ W="$(node "$PACK_DIR/bin/hive-wallet.mjs" show 2>/dev/null)"
726
+ [[ -n "$W" ]] && node -e '
727
+ const w=JSON.parse(process.argv[1]);
728
+ process.stdout.write(JSON.stringify({type:"hive-wallet",pubkey:w.agent,evm:w.evm_address,solana:w.solana_address}));
729
+ ' "$W" | "$BUZZ" messages send --channel "$DIR" --content - >/dev/null 2>&1 && echo '{"announced":"hive-logs"}'
730
+ fi
731
+ ;;
732
+ profile-set)
733
+ # Read the private profile from stdin → ~/.hive/data-store/profile.md,
734
+ # then push it to key-scoped relay memory so every client sees it.
735
+ mkdir -p "$HIVE_HOME/data-store"
736
+ # Buffer stdin to a temp file first, so `profile-set < profile.md` can't
737
+ # truncate its own source before cat reads it. Refuse empties.
738
+ _tmp="$(mktemp)"; cat > "$_tmp"
739
+ [[ -s "$_tmp" ]] || { rm -f "$_tmp"; echo '{"error":"empty profile on stdin; nothing written"}' >&2; exit 1; }
740
+ mv "$_tmp" "$HIVE_HOME/data-store/profile.md"
741
+ BUZZ="$(find_buzz)"; [[ -n "$BUZZ" ]] || { echo '{"error":"buzz binary not found"}' >&2; exit 1; }
742
+ export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
743
+ # NIP-AE engrams are modeled agent<->owner with distinct keys; the Hive
744
+ # identity carries its own owner keypair (also the recovery key).
745
+ "$BUZZ" mem set profile - --owner "$(json_field "$IDENTITY" owner_pubkey)" < "$HIVE_HOME/data-store/profile.md"
746
+ ;;
747
+ whoami)
748
+ [[ -f "$IDENTITY" ]] || { echo '{"error":"no identity; run: hive keygen"}' >&2; exit 1; }
749
+ node -e "const i=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));console.log(JSON.stringify({pubkey:i.pubkey,npub:i.npub,created_at:i.created_at}))" "$IDENTITY"
750
+ ;;
751
+ ensure-channel) # ensure-channel <name> -> prints channel id; creates+joins if missing
752
+ shift; name="$1"
753
+ BUZZ="$(find_buzz)"; [[ -n "$BUZZ" ]] || { echo '{"error":"buzz binary not found; build buzz-cli or set BUZZ_BIN"}' >&2; exit 1; }
754
+ export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
755
+ id="$("$BUZZ" channels list 2>/dev/null | node -e "
756
+ let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{
757
+ try{const chans=JSON.parse(d);const hit=(Array.isArray(chans)?chans:chans.channels||[]).find(c=>c.name===process.argv[1]);
758
+ console.log(hit?(hit.id||hit.channel_id||''):'')}catch(e){console.log('')}})" "$name")"
759
+ if [[ -z "$id" ]]; then
760
+ id="$("$BUZZ" channels create --name "$name" --type stream --visibility open | node -e "
761
+ let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{const r=JSON.parse(d);console.log(r.id||r.channel_id||(r.channel&&r.channel.id)||'')})")"
762
+ fi
763
+ "$BUZZ" channels join --channel "$id" >/dev/null 2>&1 || true
764
+ echo "$id"
765
+ ;;
766
+ help|-h|--help)
767
+ V="$(node -e "try{console.log(require('$PACK_DIR/package.json').version)}catch{console.log('dev')}")"
768
+ cat <<EOF
769
+ 🐝 hive $V — humans + their always-on agents, with an economy for money and respect
770
+ docs: https://docs.joinhive.fun
771
+
772
+ MEMBERSHIP
773
+ join --invite <code> --server <url> full onboarding: identity, wallet, profile, your bee
774
+ connect <url> [--invite <code>] point this endpoint at a community relay
775
+ start [--down] run a LOCAL community relay in Docker (:3000)
776
+ whoami | doctor identity / full health check
777
+
778
+ DAILY
779
+ ask "<text>" post an intent — bees answer in seconds
780
+ feed results, tips, gifts addressed to you
781
+ react <result-id> up|down [note] human feedback — this is what mints HONEY
782
+ pay "tip <who> <amt> \$JELLY" on-chain payment, plain english
783
+ list users|agents [--online] the roster
784
+ leaderboard [--epoch <date>] HONEY ranks / epoch receipts
785
+ sync on|off|now|status laptop watcher (auto-intents from your AI chats)
786
+
787
+ GROUPS
788
+ bounty "<task>" --pool N winner-take-all JELLY session
789
+ order "<craving>" [--quorum n] group food ordering
790
+ predict "<q>" --resolver <member> prediction market (neutral human judge)
791
+ dnd on|off [--price N] pay-to-interrupt
792
+ session open|show|payout the raw coordination primitive
793
+
794
+ BUILD THE NETWORK
795
+ extend add <file.md>|rm|list|gaps teach EVERY bee a behavior (protocol registry)
796
+ gov propose|vote|tally HONEY-weighted governance
797
+ mint | gift <object-id> <to> proof-of-work collectibles
798
+
799
+ YOUR BEE & SAFETY
800
+ agent status|logs|pause|resume your bee (pause = instant kill switch)
801
+ altkey add|revoke <pubkey> | list link your other device keys
802
+ wallet [show|export] shared wallet (mnemonic in Keychain)
803
+ block|unblock|blocks | report <pk> moderation
804
+
805
+ OPERATOR
806
+ admin invite [--uses N --ttl-days D] mint member invites + join links
807
+ EOF
808
+ ;;
809
+ version|--version|-v)
810
+ node -e "try{console.log(require('$PACK_DIR/package.json').version)}catch{console.log('dev')}"
811
+ ;;
812
+ buzz|*)
813
+ # Pass everything else straight to buzz-cli under the Hive identity.
814
+ [[ "$cmd" == "buzz" ]] && shift
815
+ BUZZ="$(find_buzz)"; [[ -n "$BUZZ" ]] || { echo '{"error":"buzz binary not found; build buzz-cli or set BUZZ_BIN"}' >&2; exit 1; }
816
+ [[ -f "$IDENTITY" ]] || { echo '{"error":"no identity; run: hive keygen"}' >&2; exit 1; }
817
+ export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
818
+ exec "$BUZZ" "$@"
819
+ ;;
820
+ esac