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 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, and settlements addressed to you (V1/V11)
276
- # Member laptops have no buzz binary the Node client covers them.
277
- if [[ -z "$(find_buzz)" ]]; then exec node "$PACK_DIR/bin/hive-net.mjs" feed; fi
278
- BUZZ="$(find_buzz)"; export BUZZ_PRIVATE_KEY="$(json_field "$IDENTITY" privkey)"
279
- PK="$(json_field "$IDENTITY" pubkey)"
280
- LOGS="$("$0" ensure-channel hive-logs)"
281
- "$BUZZ" messages get --channel "$LOGS" --limit 400 | node -e '
282
- let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{
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 "<english>" — natural-language tip, e.g. hive pay "tip siddharth 1 $JELLY" (HUMAN-gated on-chain send)
469
- shift; PHRASE="$*"
470
- [[ -n "$PHRASE" ]] || { echo '{"error":"usage: hive pay \"tip <name> <amount> $JELLY|$HONEY\""}' >&2; exit 1; }
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
- exec "$0" session open --kind predict --resolver "$RPK" --deadline "$DL" --quorum 2 "${POOLARGS[@]}" "$Qs"
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> up|down [note] human feedbackthis is what mints HONEY
793
- pay "tip <who> <amt> \$JELLY" on-chain payment, plain english
760
+ react <result-id> <emoji|word> reward an answermints 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)} · protocols: ${(j.protocols_used || []).join(',')}] react: hive react ${m.id} up`); }
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; this is what
195
- // mints HONEY at the next epoch. The result's author is resolved from the
196
- // SIGNER (never a self-asserted field).
197
- const [rid, dir = 'up', ...noteParts] = rest;
198
- if (!rid || !['up', 'down'].includes(dir)) { console.error(JSON.stringify({ error: 'usage: hive react <result-event-id> up|down [note]' })); process.exit(1); }
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({ reacted: dir, result: rid.slice(0, 12), event: r.event_id }));
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
 
@@ -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
- export const profileOverlap = (intent, profileText) => {
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 prof = ` ${String(profileText).toLowerCase()} `;
35
- return words.some((w) => prof.includes(` ${w} `) || prof.includes(`${w},`) || prof.includes(`${w}.`));
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
- export const shouldAnswer = ({ intentEventId, intent, beneficiary, selfPubkey, ownerPubkey, matchedProtocols, profileText, topK, roster, alwaysEligible = false }) => {
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' };