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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/hive +820 -0
- package/bin/hive-claim-invite.mjs +88 -0
- package/bin/hive-join.mjs +243 -0
- package/bin/hive-keygen.mjs +48 -0
- package/bin/hive-mint.mjs +65 -0
- package/bin/hive-net.mjs +400 -0
- package/bin/hive-wallet.mjs +120 -0
- package/bin/hived.mjs +5 -0
- package/bin/setup-queen.sh +71 -0
- package/daemon/engines/anthropic.mjs +45 -0
- package/daemon/engines/cli.mjs +25 -0
- package/daemon/engines/index.mjs +84 -0
- package/daemon/engines/openai.mjs +42 -0
- package/daemon/fanout.mjs +47 -0
- package/daemon/hived.mjs +782 -0
- package/daemon/relay/client.mjs +196 -0
- package/daemon/relay/cursor.mjs +59 -0
- package/daemon/relay/ws.mjs +100 -0
- package/dev/compose.yml +109 -0
- package/docs/README.md +30 -0
- package/docs/SUMMARY.md +20 -0
- package/docs/a2a-events.md +82 -0
- package/docs/architecture.md +86 -0
- package/docs/cli.md +70 -0
- package/docs/concepts.md +50 -0
- package/docs/contracts.md +85 -0
- package/docs/http-api.md +78 -0
- package/docs/protocols.md +64 -0
- package/docs/quickstart.md +51 -0
- package/docs/security.md +53 -0
- package/docs/self-hosting.md +101 -0
- package/docs/tokenomics.md +63 -0
- package/install-remote.sh +49 -0
- package/join.sh +81 -0
- package/onchain/deploy-v2.sh +82 -0
- package/onchain/deployments.sepolia.json +14 -0
- package/onchain/foundry.toml +11 -0
- package/onchain/migrate-v2.mjs +76 -0
- package/onchain/src/Honey.sol +45 -0
- package/onchain/src/HoneyV2.sol +74 -0
- package/onchain/src/Jelly.sol +19 -0
- package/onchain/src/JellyV2.sol +31 -0
- package/package.json +72 -0
- package/protocols/book-recs.md +11 -0
- package/protocols/email-in-style.md +15 -0
- package/protocols/event-hunt.md +17 -0
- package/protocols/food-order.md +20 -0
- package/protocols/group-diagnosis.md +13 -0
- package/protocols/meta.md +11 -0
- package/protocols/movie-recs.md +17 -0
- package/protocols/predict.md +21 -0
- package/protocols/read-what-others-read.md +14 -0
- package/protocols/session-bounty.md +11 -0
- package/protocols/session-split-pool.md +10 -0
- package/server/Dockerfile +33 -0
- package/server/api.mjs +192 -0
- package/server/join-page.mjs +169 -0
- package/server/keygen-treasury.mjs +33 -0
- package/server/provision.mjs +262 -0
- package/server/rewarder.mjs +369 -0
- package/server/supervisor.mjs +237 -0
- package/server/treasury.mjs +172 -0
- package/shared/config-schema.mjs +94 -0
- package/shared/events.mjs +47 -0
- package/shared/nip-oa.mjs +56 -0
- package/shared/nip98.mjs +41 -0
- package/shared/redact.mjs +20 -0
- package/shared/rewards.json +33 -0
- package/shared/sealed.mjs +50 -0
- package/shared/txqueue.mjs +42 -0
- package/skills/hive-capability-store/SKILL.md +49 -0
- package/skills/hive-data-store/SKILL.md +60 -0
- package/skills/hive-join/SKILL.md +86 -0
- package/skills/hive-object-store/SKILL.md +45 -0
- package/skills/hive-prompt/SKILL.md +54 -0
- package/skills/hive-protocol-author/SKILL.md +92 -0
- package/skills/hive-wallet/SKILL.md +54 -0
- package/watcher/distill.mjs +248 -0
- package/watcher/global.nfh.hive.sync.plist.tmpl +20 -0
- 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
|