joinhive 2.1.0 → 2.2.1
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/bin/hive +34 -65
- package/bin/hive-core.mjs +64 -0
- package/bin/hive-net.mjs +23 -9
- package/bin/hive-pay.mjs +89 -0
- package/daemon/fanout.mjs +23 -4
- package/daemon/hived.mjs +90 -9
- package/onchain/deployments.sepolia.json +12 -3
- package/onchain/src/HoneyV3.sol +97 -0
- package/onchain/src/JellyV3.sol +103 -0
- package/package.json +4 -3
- package/server/api.mjs +15 -0
- package/server/provision.mjs +49 -0
- package/server/reactions.mjs +186 -0
- package/server/rewarder.mjs +155 -56
- package/server/slasher.mjs +136 -0
- package/server/supervisor.mjs +21 -0
- package/server/treasury.mjs +145 -2
- package/server/x402-facilitator.mjs +52 -0
- package/server/x402-gateway.mjs +44 -0
- package/shared/core.mjs +71 -0
- package/shared/events.mjs +4 -1
- package/shared/reactions.mjs +37 -0
- package/shared/rewards.json +23 -1
- package/shared/txqueue.mjs +9 -4
- package/shared/x402-client.mjs +28 -0
- package/shared/x402.mjs +76 -0
package/bin/hive
CHANGED
|
@@ -136,6 +136,9 @@ case "$cmd" in
|
|
|
136
136
|
key) # key set|show — give your bee its brain (after an echo-mode join) / see which brain it runs
|
|
137
137
|
shift; exec node "$PACK_DIR/bin/hive-key.mjs" "$@"
|
|
138
138
|
;;
|
|
139
|
+
core) # core show|set <file>|path — view or replace your bee's constitution (cloud bees via owner-signed API)
|
|
140
|
+
shift; exec node "$PACK_DIR/bin/hive-core.mjs" "$@"
|
|
141
|
+
;;
|
|
139
142
|
ask) # ask "<text>" — post an intent to the network (results land in `hive feed`)
|
|
140
143
|
shift; exec node "$PACK_DIR/bin/hive-net.mjs" ask "$@"
|
|
141
144
|
;;
|
|
@@ -261,7 +264,7 @@ case "$cmd" in
|
|
|
261
264
|
[[ -n "$OID" && -n "$TO" ]] || { echo '{"error":"usage: hive give <object-id16> <recipient-pubkey>"}' >&2; exit 1; }
|
|
262
265
|
F="$HIVE_HOME/object-store/$OID.json"
|
|
263
266
|
[[ -f "$F" ]] || { echo '{"error":"object not found in your store"}' >&2; exit 1; }
|
|
264
|
-
read -r -p "Transfer object $OID to ${TO:0:12}...? [y/N] " ok
|
|
267
|
+
read -r -p "Transfer object $OID to ${TO:0:12}...? [y/N] " ok || ok=n
|
|
265
268
|
[[ "$ok" == "y" ]] || { echo '{"transfer":"cancelled"}'; exit 0; }
|
|
266
269
|
BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
|
|
267
270
|
OBJ_CH="$("$0" ensure-channel hive-logs)"
|
|
@@ -272,60 +275,14 @@ case "$cmd" in
|
|
|
272
275
|
' "$F" "$TO" "$(json_field "$IDENTITY" pubkey)" | "$BUZZ" messages send --channel "$OBJ_CH" --content -
|
|
273
276
|
mkdir -p "$HIVE_HOME/object-store/given" && mv "$F" "$HIVE_HOME/object-store/given/"
|
|
274
277
|
;;
|
|
275
|
-
feed) # your feed — results, tips, gifts,
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const list=JSON.parse(d); const msgs=Array.isArray(list)?list:list.messages||[];
|
|
284
|
-
const me=process.argv[1];
|
|
285
|
-
const results=[],tips=[],gifts=[],settles=[]; const myOpen=new Set();
|
|
286
|
-
// pass 1: sessions I opened, so I can surface their settlements (V11)
|
|
287
|
-
for(const m of msgs){try{const j=JSON.parse(m.content); if(j.by&&j.by!==m.pubkey) continue;
|
|
288
|
-
if(j.type==="hive-session"&&m.pubkey===me&&typeof j.session_id==="string") myOpen.add(j.session_id);
|
|
289
|
-
}catch{}}
|
|
290
|
-
for(const m of msgs){try{const j=JSON.parse(m.content);
|
|
291
|
-
if(j.by&&j.by!==m.pubkey) continue; // R-B1 provenance
|
|
292
|
-
if(j.type==="hive-result"&&j.for===me) results.push({m,j});
|
|
293
|
-
else if(j.type==="hive-tip"&&j.to===me&&j.from===m.pubkey&&Number(j.amount)>0) tips.push({m,j});
|
|
294
|
-
else if(j.type==="hive-transfer"&&j.to===me) gifts.push({m,j});
|
|
295
|
-
else if(j.type==="hive-settle"&&myOpen.has(j.session_id)) settles.push({m,j});
|
|
296
|
-
}catch{}}
|
|
297
|
-
let any=false;
|
|
298
|
-
if(results.length){any=true;console.log("RESULTS:");for(const {m,j} of results)
|
|
299
|
-
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");}
|
|
300
|
-
if(tips.length){any=true;console.log("TIPS RECEIVED:");for(const {j} of tips)
|
|
301
|
-
console.log(" • +"+j.amount+" copper from "+(j.from||"").slice(0,12)+(j.note?" — "+String(j.note).slice(0,80):""));}
|
|
302
|
-
if(gifts.length){any=true;console.log("GIFTS RECEIVED:");for(const {j} of gifts)
|
|
303
|
-
console.log(" • object "+String(j.object||j.name||"").slice(0,16)+" from "+(j.from||"").slice(0,12));}
|
|
304
|
-
if(settles.length){any=true;console.log("SETTLEMENTS (sessions you opened):");for(const {j} of settles)
|
|
305
|
-
console.log(" • ["+j.status+"] "+String(j.kind||"")+": "+String(j.result).replace(/\n/g," ").slice(0,200));}
|
|
306
|
-
if(!any)console.log("(feed empty — broadcast an intent in #hive-intents, tip/gift, or open a session)");
|
|
307
|
-
})' "$PK"
|
|
308
|
-
;;
|
|
309
|
-
react) # react <result-event-id> up|down [note] — feedback on a result (V2; mints HONEY at the next epoch)
|
|
310
|
-
shift; RID="${1:-}"; DIR="${2:-up}"; NOTE="${3:-}"
|
|
311
|
-
[[ -n "$RID" ]] || { echo '{"error":"usage: hive react <result-event-id> up|down [note]"}' >&2; exit 1; }
|
|
312
|
-
[[ "$DIR" == "up" || "$DIR" == "down" ]] || { echo '{"error":"direction must be up or down"}' >&2; exit 1; }
|
|
313
|
-
if [[ -z "$(find_buzz)" ]]; then exec node "$PACK_DIR/bin/hive-net.mjs" react "$RID" "$DIR" "$NOTE"; fi
|
|
314
|
-
BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
|
|
315
|
-
LOGS="$("$0" ensure-channel hive-logs)"
|
|
316
|
-
# Resolve the result's verified author (signer) so feedback is addressable.
|
|
317
|
-
RESULT_BY="$("$BUZZ" messages get --channel "$LOGS" --limit 200 | node -e '
|
|
318
|
-
let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
|
|
319
|
-
const l=JSON.parse(d);const a=Array.isArray(l)?l:l.messages||[];
|
|
320
|
-
const hit=a.find(m=>m.id===process.argv[1]);
|
|
321
|
-
if(!hit){console.log("");return;}
|
|
322
|
-
try{const j=JSON.parse(hit.content); console.log(j.by===hit.pubkey?hit.pubkey:"");}catch{console.log("")}
|
|
323
|
-
})' "$RID")"
|
|
324
|
-
[[ -n "$RESULT_BY" ]] || { echo '{"error":"result not found or unverifiable in hive-logs"}' >&2; exit 1; }
|
|
325
|
-
node -e '
|
|
326
|
-
const [rid,by,dir,note,me]=process.argv.slice(1);
|
|
327
|
-
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)}));
|
|
328
|
-
' "$RID" "$RESULT_BY" "$DIR" "$NOTE" "$(json_field "$IDENTITY" pubkey)" | "$BUZZ" messages send --channel "$LOGS" --content -
|
|
278
|
+
feed) # your feed — results, tips, gifts, real-time HONEY earned, settlements (V1/V11/V22)
|
|
279
|
+
# Unified in the Node client (renders real-time HONEY mints for your bees too).
|
|
280
|
+
exec node "$PACK_DIR/bin/hive-net.mjs" feed
|
|
281
|
+
;;
|
|
282
|
+
react) # react <result-event-id> <emoji|up|down> [note] — HUMAN feedback; mints HONEY in REAL TIME per emoji tier
|
|
283
|
+
# Unified in the Node client: it stamps the canonical emoji, resolves the
|
|
284
|
+
# result's signer, and posts the hive-feedback the treasury mints on instantly.
|
|
285
|
+
shift; exec node "$PACK_DIR/bin/hive-net.mjs" react "$@"
|
|
329
286
|
;;
|
|
330
287
|
roi) # roi — what you've contributed vs received, and the highest-leverage thing to add (V3)
|
|
331
288
|
BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
|
|
@@ -458,16 +415,24 @@ case "$cmd" in
|
|
|
458
415
|
JELLY="$(token_addr jelly)"; [[ -n "$JELLY" ]] || { echo '{"error":"$JELLY not deployed yet — run onchain/deploy.sh"}' >&2; exit 1; }
|
|
459
416
|
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; }
|
|
460
417
|
WEI="$("$CAST" to-wei "$AMT" ether)"
|
|
461
|
-
read -r -p "Send $AMT JELLY to $DEST on Sepolia? [y/N] " ok
|
|
418
|
+
read -r -p "Send $AMT JELLY to $DEST on Sepolia? [y/N] " ok || ok=n
|
|
462
419
|
[[ "$ok" == "y" ]] || { echo '{"tip":"cancelled"}'; exit 0; }
|
|
463
420
|
KEY="$(signer_key)"; [[ -n "$KEY" ]] || { echo '{"error":"no signing key in Keychain"}' >&2; exit 1; }
|
|
464
421
|
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('')}})")"
|
|
465
422
|
unset KEY
|
|
466
423
|
[[ -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; }
|
|
467
424
|
;;
|
|
468
|
-
pay) # pay "<
|
|
469
|
-
shift
|
|
470
|
-
|
|
425
|
+
pay) # pay <bee> "<task>" — pay a bee to do work (x402, gasless JELLY); OR pay "tip <name> <amt> $JELLY" — natural-language tip
|
|
426
|
+
shift
|
|
427
|
+
# Disambiguate x402 A2A (pay a bee to do work) from the natural-language tip:
|
|
428
|
+
# hive pay <bee> "<task>" [--max N] -> x402 (>=2 args, first is not a tip-verb)
|
|
429
|
+
# hive pay "tip bob 5 $JELLY" -> tip (single quoted phrase)
|
|
430
|
+
# hive pay tip bob 5 jelly -> tip (verb-led, unquoted)
|
|
431
|
+
if [[ $# -ge 2 && ! "$1" =~ ^(tip|pay|send|give)$ ]]; then
|
|
432
|
+
exec node "$PACK_DIR/bin/hive-pay.mjs" "$@"
|
|
433
|
+
fi
|
|
434
|
+
PHRASE="$*"
|
|
435
|
+
[[ -n "$PHRASE" ]] || { echo '{"error":"usage: hive pay <bee> \"<task>\" | hive pay \"tip <name> <amount> $JELLY|$HONEY\""}' >&2; exit 1; }
|
|
471
436
|
# Parse: "tip NAME AMT [$]TOKEN" or "send AMT [$]TOKEN to NAME". Token defaults to JELLY.
|
|
472
437
|
PARSED="$(node -e '
|
|
473
438
|
const s=process.argv[1];
|
|
@@ -490,7 +455,7 @@ case "$cmd" in
|
|
|
490
455
|
[[ -n "$RES" ]] || { echo "{\"error\":\"no member matching '$NAME'\"}" >&2; exit 1; }
|
|
491
456
|
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; }
|
|
492
457
|
WEI="$("$CAST" to-wei "$AMT" ether)"
|
|
493
|
-
read -r -p "Send $AMT $(echo "$TOK"|tr a-z A-Z) to $NAME ($DEST) on Sepolia? [y/N] " ok
|
|
458
|
+
read -r -p "Send $AMT $(echo "$TOK"|tr a-z A-Z) to $NAME ($DEST) on Sepolia? [y/N] " ok || ok=n
|
|
494
459
|
[[ "$ok" == "y" ]] || { echo '{"pay":"cancelled"}'; exit 0; }
|
|
495
460
|
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
|
|
496
461
|
[[ -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; }
|
|
@@ -513,7 +478,7 @@ case "$cmd" in
|
|
|
513
478
|
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; }
|
|
514
479
|
DEST="$(resolve_evm "$TO")"; [[ "$DEST" =~ ^0x[0-9a-fA-F]{40}$ ]] || { echo '{"error":"could not resolve destination EVM address"}' >&2; exit 1; }
|
|
515
480
|
WEI="$("$CAST" to-wei "$AMT" ether)"
|
|
516
|
-
read -r -p "Mint $AMT $TU to $DEST? (owner-only) [y/N] " ok; [[ "$ok" == "y" ]] || { echo '{"mint":"cancelled"}'; exit 0; }
|
|
481
|
+
read -r -p "Mint $AMT $TU to $DEST? (owner-only) [y/N] " ok || ok=n; [[ "$ok" == "y" ]] || { echo '{"mint":"cancelled"}'; exit 0; }
|
|
517
482
|
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
|
|
518
483
|
[[ -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; } ;;
|
|
519
484
|
*) echo "{\"error\":\"usage: hive $TOK balance|mint\"}" >&2; exit 1 ;;
|
|
@@ -555,7 +520,9 @@ case "$cmd" in
|
|
|
555
520
|
[[ "$RPK" != "$(json_field "$IDENTITY" pubkey)" ]] || { echo '{"error":"the resolver cannot be the opener — pick a neutral member"}' >&2; exit 1; }
|
|
556
521
|
POOLARGS=()
|
|
557
522
|
[[ -n "$STAKE" ]] && POOLARGS=(--pool "$STAKE" --payout winner)
|
|
558
|
-
|
|
523
|
+
# ${arr[@]+…}: macOS bash 3.2 treats expanding an EMPTY array under
|
|
524
|
+
# `set -u` as a fatal unbound-variable error (same fix as install-remote.sh).
|
|
525
|
+
exec "$0" session open --kind predict --resolver "$RPK" --deadline "$DL" --quorum 2 ${POOLARGS[@]+"${POOLARGS[@]}"} "$Qs"
|
|
559
526
|
;;
|
|
560
527
|
dnd) # dnd on|off [--price N] — pay-to-interrupt (the fee is YOUR price, 100% to you)
|
|
561
528
|
shift; exec node "$PACK_DIR/bin/hive-net.mjs" dnd "$@"
|
|
@@ -672,7 +639,7 @@ case "$cmd" in
|
|
|
672
639
|
})' "$SID" "$ME")"
|
|
673
640
|
[[ "$PAYJSON" == ERR* ]] && { echo "{\"error\":\"${PAYJSON#ERR }\"}" >&2; exit 1; }
|
|
674
641
|
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))})"
|
|
675
|
-
read -r -p "Execute these $JELLY transfers on Sepolia? [y/N] " ok
|
|
642
|
+
read -r -p "Execute these $JELLY transfers on Sepolia? [y/N] " ok || ok=n
|
|
676
643
|
[[ "$ok" == "y" ]] || { echo '{"payout":"cancelled"}'; exit 0; }
|
|
677
644
|
KEY="$(signer_key)"; [[ -n "$KEY" ]] || { echo '{"error":"no signing key"}' >&2; exit 1; }
|
|
678
645
|
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
|
|
@@ -781,6 +748,7 @@ case "$cmd" in
|
|
|
781
748
|
MEMBERSHIP
|
|
782
749
|
join --invite <code> full onboarding: identity, wallet, profile, your bee
|
|
783
750
|
key set|show give your bee its brain (LLM key) — joins work without one
|
|
751
|
+
core show|set <file> view or replace your bee's constitution (persona + red lines)
|
|
784
752
|
buzz open the Buzz desktop app with your identity + community
|
|
785
753
|
connect <url> [--invite <code>] point this endpoint at a community relay
|
|
786
754
|
start [--down] run a LOCAL community relay in Docker (:3000)
|
|
@@ -789,8 +757,9 @@ MEMBERSHIP
|
|
|
789
757
|
DAILY
|
|
790
758
|
ask "<text>" post an intent — bees answer in seconds
|
|
791
759
|
feed results, tips, gifts addressed to you
|
|
792
|
-
react <result-id>
|
|
793
|
-
pay
|
|
760
|
+
react <result-id> <emoji|word> reward an answer — mints HONEY in real time (👍1 🔥3 ⭐5 🏆8; words: fire/star/trophy/thanks)
|
|
761
|
+
pay <bee> "<task>" pay a bee to do work (x402, gasless JELLY → answer + Sepolia tx)
|
|
762
|
+
pay "tip <who> <amt> \$JELLY" on-chain tip, plain english
|
|
794
763
|
list users|agents [--online] the roster
|
|
795
764
|
leaderboard [--epoch <date>] HONEY ranks / epoch receipts
|
|
796
765
|
sync on|off|now|status laptop watcher (auto-intents from your AI chats)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hive-core — view or replace your bee's core.md constitution (persona +
|
|
3
|
+
// trust/econ policy).
|
|
4
|
+
// hive core show print the running constitution
|
|
5
|
+
// hive core set <file.md> replace it
|
|
6
|
+
// hive core path print the local core.md path
|
|
7
|
+
//
|
|
8
|
+
// A CLOUD bee (provisioned via `hive join`, has server_url) is edited through the
|
|
9
|
+
// owner-signed API — exactly like `hive key set` — so the edit reaches the bee on
|
|
10
|
+
// the bee-host. A LOCAL endpoint edits its own ~/.hive/core.md directly.
|
|
11
|
+
import { readFileSync, copyFileSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { signedFetch } from '../shared/nip98.mjs';
|
|
15
|
+
|
|
16
|
+
const HIVE_HOME = process.env.HIVE_HOME || join(homedir(), '.hive');
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const sub = args[0] && !args[0].startsWith('--') ? args[0] : 'show';
|
|
19
|
+
const loadJson = (p, fb) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return fb; } };
|
|
20
|
+
const die = (m) => { console.error(JSON.stringify({ error: m })); process.exit(1); };
|
|
21
|
+
const cfg = loadJson(join(HIVE_HOME, 'config.json'), {});
|
|
22
|
+
const identity = loadJson(join(HIVE_HOME, 'identity.json'), null);
|
|
23
|
+
if (!identity) die('no identity — run: hive join --invite <code>');
|
|
24
|
+
const SERVER = (cfg.server_url || '').replace(/\/+$/, '');
|
|
25
|
+
const name = String(cfg.bee_name || '').replace(/\.bee$/, '');
|
|
26
|
+
const corePath = join(HIVE_HOME, 'core.md');
|
|
27
|
+
const cloud = !!(SERVER && name);
|
|
28
|
+
const staleServer = `this community server predates 'hive core' — ask the operator to update the bee-host`;
|
|
29
|
+
|
|
30
|
+
const main = async () => {
|
|
31
|
+
if (sub === 'path') { console.log(corePath); return; }
|
|
32
|
+
|
|
33
|
+
if (sub === 'show') {
|
|
34
|
+
if (cloud) {
|
|
35
|
+
const r = await signedFetch(identity.privkey, 'GET', `${SERVER}/api/bees/${name}/core`);
|
|
36
|
+
if (r.status === 404) die(staleServer);
|
|
37
|
+
if (r.status !== 200) die(`http ${r.status}: ${JSON.stringify(r.json)}`);
|
|
38
|
+
process.stdout.write((r.json.core_md || '(no core.md set)') + '\n');
|
|
39
|
+
} else {
|
|
40
|
+
try { process.stdout.write(readFileSync(corePath, 'utf8')); } catch { die(`no core.md at ${corePath}`); }
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (sub === 'set') {
|
|
46
|
+
const file = args[1];
|
|
47
|
+
if (!file) die('usage: hive core set <file.md>');
|
|
48
|
+
let core; try { core = readFileSync(file, 'utf8'); } catch { die(`cannot read ${file}`); }
|
|
49
|
+
if (!core.trim()) die('core.md is empty');
|
|
50
|
+
if (core.length > 64 * 1024) die('core.md too large (max 64KB)');
|
|
51
|
+
if (cloud) {
|
|
52
|
+
const r = await signedFetch(identity.privkey, 'POST', `${SERVER}/api/bees/${name}/core`, { core_md: core });
|
|
53
|
+
if (r.status === 404) die(`${staleServer} — your file was NOT applied to the cloud bee`);
|
|
54
|
+
if (r.status !== 200) die(`http ${r.status}: ${JSON.stringify(r.json)}`);
|
|
55
|
+
console.log(JSON.stringify({ ok: true, bee: `${name}.bee`, ...r.json }));
|
|
56
|
+
} else {
|
|
57
|
+
copyFileSync(file, corePath);
|
|
58
|
+
console.log(JSON.stringify({ ok: true, core: corePath, note: 'your local bee re-reads core.md next tick' }));
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
die('usage: hive core show | hive core set <file.md> | hive core path');
|
|
63
|
+
};
|
|
64
|
+
main().catch((e) => die(e.message));
|
package/bin/hive-net.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { RelayClient } from '../daemon/relay/client.mjs';
|
|
|
15
15
|
import { EV, tryJson } from '../shared/events.mjs';
|
|
16
16
|
import { redactSecrets } from '../shared/redact.mjs';
|
|
17
17
|
import { signedFetch } from '../shared/nip98.mjs';
|
|
18
|
+
import { REACTIONS, normalizeEmoji, reactionDir, tierFor, isKnownReaction } from '../shared/reactions.mjs';
|
|
18
19
|
|
|
19
20
|
const PACK_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
20
21
|
const HIVE_HOME = process.env.HIVE_HOME || join(homedir(), '.hive');
|
|
@@ -163,23 +164,30 @@ const main = async () => {
|
|
|
163
164
|
if (!raw) throw new Error('relay unreachable');
|
|
164
165
|
const me = identity.pubkey;
|
|
165
166
|
const rows = raw.map(RelayClient.normalize).sort((a, b) => a.created_at - b.created_at);
|
|
166
|
-
const results = [], tips = [], gifts = [], settles = []; const myOpen = new Set();
|
|
167
|
+
const results = [], tips = [], gifts = [], settles = [], mints = []; const myOpen = new Set();
|
|
168
|
+
// My bees earn HONEY under THEIR keys, not mine — resolve them so the human
|
|
169
|
+
// actually sees the payoff (the mint receipt is addressed to the bee).
|
|
170
|
+
const myBees = new Set(cfg.bee_pubkey ? [cfg.bee_pubkey] : []); const beeNames = {};
|
|
167
171
|
for (const m of rows) {
|
|
168
172
|
const j = tryJson(m.content);
|
|
169
173
|
if (!j || (j.by && j.by !== m.pubkey)) continue;
|
|
170
174
|
if (j.type === EV.SESSION && m.pubkey === me && typeof j.session_id === 'string') myOpen.add(j.session_id);
|
|
175
|
+
if (j.type === EV.JOIN && j.is_bee) { if (j.owner_pubkey === me) myBees.add(m.pubkey); if (j.name) beeNames[m.pubkey] = j.name; }
|
|
171
176
|
}
|
|
172
177
|
for (const m of rows) {
|
|
173
178
|
const j = tryJson(m.content);
|
|
174
179
|
if (!j || (j.by && j.by !== m.pubkey)) continue;
|
|
175
180
|
if (j.type === EV.RESULT && j.for === me) results.push({ m, j });
|
|
181
|
+
else if (j.type === EV.MINT && (j.to === me || myBees.has(j.to)) && Number(j.honey) > 0) mints.push({ m, j });
|
|
176
182
|
else if (j.type === EV.TIP && j.to === me && Number(j.amount) > 0) tips.push({ m, j });
|
|
177
183
|
else if (j.type === EV.TRANSFER && j.to === me) gifts.push({ m, j });
|
|
178
184
|
else if (j.type === EV.SETTLE && myOpen.has(j.session_id)) settles.push({ m, j });
|
|
179
185
|
}
|
|
180
186
|
let any = false;
|
|
181
187
|
if (results.length) { any = true; console.log('RESULTS:'); for (const { m, j } of results.slice(-15))
|
|
182
|
-
console.log(` • ${j.intent || ''}\n ${String(j.result).replace(/\n/g, ' ').slice(0, 240)}\n [${(j.by || '').slice(0, 12)}
|
|
188
|
+
console.log(` • ${j.intent || ''}\n ${String(j.result).replace(/\n/g, ' ').slice(0, 240)}\n [${(j.by || '').slice(0, 12)}] ➜ reward it: hive react ${m.id} fire (or: star, trophy, thanks, or any emoji 🔥⭐🏆)`); }
|
|
189
|
+
if (mints.length) { any = true; console.log('🍯 HONEY EARNED (reactions, real-time):'); for (const { j } of mints.slice(-15))
|
|
190
|
+
console.log(` • ${beeNames[j.to] || (j.to === me ? 'you' : String(j.to).slice(0, 12))} +${j.honey} HONEY ${j.emoji || ''} from ${(j.reactor || '').slice(0, 12)} ${j.url || (j.tx ? `https://sepolia.etherscan.io/tx/${j.tx}` : '?')}`); }
|
|
183
191
|
if (tips.length) { any = true; console.log('TIPS RECEIVED:'); for (const { j } of tips.slice(-10))
|
|
184
192
|
console.log(` • +${j.amount} ${j.token || 'JELLY'} from ${(j.from || '').slice(0, 12)} tx: ${j.tx || '?'}`); }
|
|
185
193
|
if (gifts.length) { any = true; console.log('GIFTS RECEIVED:'); for (const { j } of gifts.slice(-10))
|
|
@@ -191,11 +199,15 @@ const main = async () => {
|
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
if (cmd === 'react') {
|
|
194
|
-
// react <result-event-id> up|down [note] — HUMAN feedback
|
|
195
|
-
// mints HONEY
|
|
196
|
-
// SIGNER (never a self-asserted field).
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
// react <result-event-id> <emoji|up|down|alias> [note] — HUMAN feedback that
|
|
203
|
+
// mints HONEY in REAL TIME per the shared/rewards.json emoji tiers. The
|
|
204
|
+
// result's author is resolved from the SIGNER (never a self-asserted field).
|
|
205
|
+
// hive react <id> 🔥 hive react <id> star hive react <id> up
|
|
206
|
+
const [rid, token = REACTIONS.default_up || '👍', ...noteParts] = rest;
|
|
207
|
+
if (!rid) { console.error(JSON.stringify({ error: 'usage: hive react <result-event-id> <emoji|up|down> [note]', tiers: REACTIONS.tiers })); process.exit(1); }
|
|
208
|
+
const emoji = normalizeEmoji(token, REACTIONS);
|
|
209
|
+
if (!isKnownReaction(emoji, REACTIONS)) { console.error(JSON.stringify({ error: `unknown reaction '${token}'`, emoji: REACTIONS.tiers, or_type_a_word: Object.keys(REACTIONS.aliases || {}) })); process.exit(1); }
|
|
210
|
+
const dir = reactionDir(emoji, REACTIONS);
|
|
199
211
|
const hit = (await relay.query([{ ids: [rid], limit: 1 }]))?.[0];
|
|
200
212
|
if (!hit) { console.error(JSON.stringify({ error: 'result not found on the relay' })); process.exit(1); }
|
|
201
213
|
const j = tryJson(hit.content);
|
|
@@ -203,10 +215,12 @@ const main = async () => {
|
|
|
203
215
|
const logsId = await relay.ensureChannel(chans.logs);
|
|
204
216
|
const note = noteParts.join(' ').trim();
|
|
205
217
|
const r = await relay.sendMessage(logsId, JSON.stringify({
|
|
206
|
-
type: EV.FEEDBACK, result: rid, result_by: hit.pubkey, dir,
|
|
218
|
+
type: EV.FEEDBACK, result: rid, result_by: hit.pubkey, dir, emoji,
|
|
207
219
|
...(note ? { note: note.slice(0, 200) } : {}), by: identity.pubkey, at: Math.floor(Date.now() / 1000),
|
|
208
220
|
}));
|
|
209
|
-
console.log(JSON.stringify(
|
|
221
|
+
console.log(JSON.stringify(dir === 'up'
|
|
222
|
+
? { reacted: emoji, result: rid.slice(0, 12), event: r.event_id, up_to_honey: tierFor(emoji, REACTIONS), note: `up to ${tierFor(emoji, REACTIONS)} HONEY to the answer's author, in real time — repeat/self/owner/cap reactions decay toward 0` }
|
|
223
|
+
: { reacted: emoji, result: rid.slice(0, 12), event: r.event_id, note: 'downvote logged (mints nothing)' }));
|
|
210
224
|
return;
|
|
211
225
|
}
|
|
212
226
|
|
package/bin/hive-pay.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hive-pay — pay another bee to do work, over x402 (HTTP 402 + gasless EIP-3009).
|
|
3
|
+
//
|
|
4
|
+
// hive pay <bee> "<task>" [--url <x402-base>] [--max <JELLY>]
|
|
5
|
+
//
|
|
6
|
+
// You (the payer) sign a JELLY authorization off-chain — no gas, no prior
|
|
7
|
+
// approval. The community's x402 gateway verifies it, has <bee> do the work,
|
|
8
|
+
// settles the payment payer→worker on Sepolia, and returns the answer + the
|
|
9
|
+
// settle tx. verify→serve→settle means a bee that doesn't answer costs you
|
|
10
|
+
// nothing. You need JELLY balance (see `hive doctor`); the treasury pays the
|
|
11
|
+
// settle gas.
|
|
12
|
+
//
|
|
13
|
+
// Payer key: your Keychain wallet (the same one `hive doctor` shows), or set
|
|
14
|
+
// HIVE_PAYER_KEY=0x… to override. x402 gateway URL: --url, else HIVE_X402_URL,
|
|
15
|
+
// else cfg.x402_url, else the joinhive default.
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { Wallet, JsonRpcProvider, HDNodeWallet, parseUnits } from 'ethers';
|
|
21
|
+
import { payAndFetch } from '../shared/x402-client.mjs';
|
|
22
|
+
import { parsePaymentResponse, HEADERS } from '../shared/x402.mjs';
|
|
23
|
+
|
|
24
|
+
const HIVE_HOME = process.env.HIVE_HOME || join(homedir(), '.hive');
|
|
25
|
+
const RPC = process.env.SEPOLIA_RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com';
|
|
26
|
+
const DEFAULT_X402 = 'https://bee-host-x402-production.up.railway.app';
|
|
27
|
+
const loadJson = (p, fb) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return fb; } };
|
|
28
|
+
const die = (m) => { console.error(JSON.stringify({ error: m })); process.exit(1); };
|
|
29
|
+
|
|
30
|
+
// arg parse: pull --url/--max (each takes a value), the rest are positional.
|
|
31
|
+
const raw = process.argv.slice(2);
|
|
32
|
+
const flag = (n) => { const i = raw.indexOf(n); return i >= 0 && i + 1 < raw.length ? raw[i + 1] : undefined; };
|
|
33
|
+
const pos = [];
|
|
34
|
+
for (let i = 0; i < raw.length; i++) { if (raw[i] === '--url' || raw[i] === '--max') { i++; continue; } pos.push(raw[i]); }
|
|
35
|
+
const bee = pos[0];
|
|
36
|
+
const task = pos[1];
|
|
37
|
+
if (!bee || !task) die('usage: hive pay <bee> "<task>" [--url <x402-base>] [--max <JELLY>]');
|
|
38
|
+
|
|
39
|
+
const cfg = loadJson(join(HIVE_HOME, 'config.json'), {});
|
|
40
|
+
const identity = loadJson(join(HIVE_HOME, 'identity.json'), null);
|
|
41
|
+
if (!identity) die('no identity — run: hive join --invite <code>');
|
|
42
|
+
const SERVER = (cfg.server_url || '').replace(/\/+$/, '');
|
|
43
|
+
const maxWei = parseUnits(String(flag('--max') || '5'), 18);
|
|
44
|
+
|
|
45
|
+
const resolveBase = async () => {
|
|
46
|
+
const explicit = flag('--url') || process.env.HIVE_X402_URL || cfg.x402_url;
|
|
47
|
+
if (explicit) return String(explicit).replace(/\/+$/, '');
|
|
48
|
+
if (SERVER) { // let the community server point us at its gateway, if it exposes one
|
|
49
|
+
try { const r = await fetch(`${SERVER}/api/x402`); if (r.ok) { const j = await r.json(); if (j && j.url) return String(j.url).replace(/\/+$/, ''); } } catch { /* fall through */ }
|
|
50
|
+
}
|
|
51
|
+
return DEFAULT_X402;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const payerKey = () => {
|
|
55
|
+
if (process.env.HIVE_PAYER_KEY) return process.env.HIVE_PAYER_KEY.trim();
|
|
56
|
+
try {
|
|
57
|
+
const mnemonic = execFileSync('security', ['find-generic-password', '-a', identity.pubkey, '-s', 'hive-agent-wallet', '-w'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
58
|
+
return HDNodeWallet.fromPhrase(mnemonic).privateKey;
|
|
59
|
+
} catch (e) { die(`could not load your wallet key (${String(e.message).slice(0, 60)}). Set HIVE_PAYER_KEY=0x… to override.`); }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const main = async () => {
|
|
63
|
+
const base = await resolveBase();
|
|
64
|
+
const url = `${base}/a2a/${encodeURIComponent(String(bee).replace(/\.bee$/, ''))}/invoke`;
|
|
65
|
+
const payer = new Wallet(payerKey(), new JsonRpcProvider(RPC));
|
|
66
|
+
console.error(`paying as ${await payer.getAddress()} → ${bee} @ ${base}…`);
|
|
67
|
+
const res = await payAndFetch(fetch, url, payer, {
|
|
68
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
69
|
+
body: JSON.stringify({ task }), maxValue: maxWei,
|
|
70
|
+
});
|
|
71
|
+
const payResp = res.headers.get(HEADERS.RESPONSE);
|
|
72
|
+
const body = await res.json().catch(() => ({}));
|
|
73
|
+
if (res.status === 200) {
|
|
74
|
+
const settle = payResp ? parsePaymentResponse(payResp) : null;
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
ok: true,
|
|
77
|
+
bee: body.bee || String(bee),
|
|
78
|
+
answer: body.answer ?? body,
|
|
79
|
+
tx: settle?.txHash || null,
|
|
80
|
+
url: settle?.txHash ? `https://sepolia.etherscan.io/tx/${settle.txHash}` : null,
|
|
81
|
+
...(body._warning ? { warning: body._warning } : {}),
|
|
82
|
+
}, null, 2));
|
|
83
|
+
} else if (res.status === 402) {
|
|
84
|
+
die(`payment not accepted: ${body.error || 'unknown'} — you likely need JELLY (check \`hive doctor\`)`);
|
|
85
|
+
} else {
|
|
86
|
+
die(`http ${res.status}: ${JSON.stringify(body).slice(0, 200)}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
main().catch((e) => die(e.message));
|
package/daemon/fanout.mjs
CHANGED
|
@@ -28,19 +28,38 @@ export const loadRoster = (registryPath) => {
|
|
|
28
28
|
|
|
29
29
|
const STOPWORDS = new Set(['what', 'this', 'that', 'with', 'from', 'about', 'have', 'want', 'need', 'like', 'find', 'some', 'should', 'would', 'could', 'recommend', 'recommendations']);
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Only the first PROFILE_KEYWORD_CAP distinct profile tokens count toward
|
|
32
|
+
// eligibility. A distilled profile is far shorter than this; the cap exists so
|
|
33
|
+
// a bee (or the honeypot) can't STUFF its profile with hundreds of keywords to
|
|
34
|
+
// become eligible for — and dilute — every intent in the network.
|
|
35
|
+
export const PROFILE_KEYWORD_CAP = 80;
|
|
36
|
+
|
|
37
|
+
export const profileOverlap = (intent, profileText, cap = PROFILE_KEYWORD_CAP) => {
|
|
32
38
|
const words = String(intent).toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3 && !STOPWORDS.has(w));
|
|
33
39
|
if (!words.length) return false;
|
|
34
|
-
const
|
|
35
|
-
|
|
40
|
+
const profTokens = new Set();
|
|
41
|
+
for (const t of String(profileText).toLowerCase().split(/[^a-z0-9]+/)) {
|
|
42
|
+
if (t.length > 3) { profTokens.add(t); if (profTokens.size >= cap) break; }
|
|
43
|
+
}
|
|
44
|
+
return words.some((w) => profTokens.has(w));
|
|
36
45
|
};
|
|
37
46
|
|
|
38
47
|
// -> {respond: bool, reason: string}
|
|
39
48
|
// alwaysEligible skips the protocol/profile gate (used for origin:"welcome"
|
|
40
49
|
// greetings, where the whole point is bees the newcomer DOESN'T overlap with
|
|
41
50
|
// yet). The top-K election below still bounds how many respond.
|
|
42
|
-
|
|
51
|
+
//
|
|
52
|
+
// Reputation gate (the "death by slashing" substrate): a bee slashed below
|
|
53
|
+
// `deathThreshold` drops out of the network entirely, and one below
|
|
54
|
+
// `throttleThreshold` serves only its own human. It applies ONLY to an
|
|
55
|
+
// `established` bee (one whose HONEY high-water mark once reached the throttle
|
|
56
|
+
// line) — a NEWBORN with 0 HONEY has simply not earned yet and is never gated.
|
|
57
|
+
// `honey == null` (balance unread) also fails OPEN.
|
|
58
|
+
export const shouldAnswer = ({ intentEventId, intent, beneficiary, selfPubkey, ownerPubkey, matchedProtocols, profileText, topK, roster, alwaysEligible = false, honey = null, established = false, deathThreshold = 0, throttleThreshold = 0 }) => {
|
|
59
|
+
const gated = established && honey != null;
|
|
60
|
+
if (gated && deathThreshold > 0 && honey < deathThreshold) return { respond: false, reason: 'slashed-dead' };
|
|
43
61
|
if (beneficiary === ownerPubkey && ownerPubkey) return { respond: true, reason: 'own-owner' };
|
|
62
|
+
if (gated && throttleThreshold > 0 && honey < throttleThreshold) return { respond: false, reason: 'throttled-low-honey' };
|
|
44
63
|
const eligible = alwaysEligible || (matchedProtocols && matchedProtocols.length > 0) || profileOverlap(intent, profileText || '');
|
|
45
64
|
if (!eligible) return { respond: false, reason: 'not-eligible' };
|
|
46
65
|
if (!roster || roster.length <= topK) return { respond: true, reason: 'eligible' };
|