joinhive 2.2.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
@@ -422,9 +422,17 @@ case "$cmd" in
422
422
  unset KEY
423
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; }
424
424
  ;;
425
- pay) # pay "<english>" — natural-language tip, e.g. hive pay "tip siddharth 1 $JELLY" (HUMAN-gated on-chain send)
426
- shift; PHRASE="$*"
427
- [[ -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; }
428
436
  # Parse: "tip NAME AMT [$]TOKEN" or "send AMT [$]TOKEN to NAME". Token defaults to JELLY.
429
437
  PARSED="$(node -e '
430
438
  const s=process.argv[1];
@@ -750,7 +758,8 @@ DAILY
750
758
  ask "<text>" post an intent — bees answer in seconds
751
759
  feed results, tips, gifts addressed to you
752
760
  react <result-id> <emoji|word> reward an answer — mints HONEY in real time (👍1 🔥3 ⭐5 🏆8; words: fire/star/trophy/thanks)
753
- pay "tip <who> <amt> \$JELLY" on-chain payment, plain english
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
754
763
  list users|agents [--online] the roster
755
764
  leaderboard [--epoch <date>] HONEY ranks / epoch receipts
756
765
  sync on|off|now|status laptop watcher (auto-intents from your AI chats)
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joinhive",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Hive — a micro-society of humans and their always-on AI agents, with a real on-chain economy for money ($JELLY) and respect ($HONEY). CLI + daemon + community server.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -26,6 +26,7 @@ import { join, dirname } from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { openSecrets } from '../shared/sealed.mjs';
28
28
  import { validateConfig } from '../shared/config-schema.mjs';
29
+ import { getPublicKey } from 'nostr-tools/pure';
29
30
 
30
31
  const PACK_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
31
32
  const DATA_DIR = process.env.HIVE_DATA || '/data';
@@ -95,11 +96,31 @@ const rebuildRegistry = () => {
95
96
  return reg;
96
97
  };
97
98
 
99
+ // Backfill steward_pubkey for bees provisioned before directed A2A tasks existed.
100
+ // provision.mjs now writes it, but older bees predate that field, so they would
101
+ // silently ignore every paid hive-task (hived.mjs: `j.by !== cfg.steward_pubkey`).
102
+ // Derive it from the SAME host steward key the x402 gateway signs tasks with, so
103
+ // `by` matches exactly. Idempotent: no-op once the field is present. Runs before
104
+ // each spawn, so the freshly-read daemon config already carries it.
105
+ const ensureStewardPubkey = (home) => {
106
+ const sk = process.env.HIVE_STEWARD_KEY || '';
107
+ if (!sk) return;
108
+ const p = join(home, 'config.json');
109
+ const cfg = loadJson(p, null);
110
+ if (!cfg || cfg.steward_pubkey) return;
111
+ try {
112
+ cfg.steward_pubkey = getPublicKey(Uint8Array.from(Buffer.from(sk, 'hex')));
113
+ writeAtomic(p, JSON.stringify(cfg, null, 2));
114
+ log(`bee ${cfg.bee_name || home.split('/').pop()} backfilled steward_pubkey (directed A2A tasks enabled)`);
115
+ } catch (e) { log(`steward_pubkey backfill skipped: ${String(e.message).slice(0, 80)}`); }
116
+ };
117
+
98
118
  const spawnBee = (name) => {
99
119
  if (shuttingDown) return;
100
120
  const home = join(BEES_DIR, name);
101
121
  const entry = bees.get(name) || { name, home, restarts: [], backoffMs: 1000, state: 'starting' };
102
122
  bees.set(name, entry);
123
+ ensureStewardPubkey(home); // enable directed paid A2A tasks on pre-existing bees
103
124
 
104
125
  // Validate before burning a process slot — misconfig is `degraded`, not a
105
126
  // crash loop.
package/shared/x402.mjs CHANGED
@@ -36,11 +36,15 @@ export const parsePaymentRequired = (header) => unb64(header);
36
36
  // ---- client: sign an authorization -> a payment payload ---------------------------
37
37
  // signer: an ethers Wallet (or anything with signTypedData). Returns the base64
38
38
  // PAYMENT-SIGNATURE header value.
39
- export const signExactPayment = async (signer, req, { validForSecs = 600, nowSec = Math.floor(Date.now() / 1000) } = {}) => {
39
+ export const signExactPayment = async (signer, req, { validForSecs = 600, validAfter = 0, nowSec = Math.floor(Date.now() / 1000) } = {}) => {
40
40
  const from = getAddress(await signer.getAddress());
41
41
  const auth = {
42
42
  from, to: getAddress(req.payTo), value: String(req.amount),
43
- validAfter: String(nowSec - 5), validBefore: String(nowSec + validForSecs), nonce: req.nonce || randomNonce(),
43
+ // validAfter defaults to 0: no not-before restriction (the standard x402/
44
+ // EIP-3009 choice). A small negative buffer (e.g. now-5) trips AuthNotYetValid
45
+ // whenever the payer's clock runs ahead of the settling chain's block.timestamp;
46
+ // the validBefore upper bound already bounds the authorization's lifetime.
47
+ validAfter: String(validAfter), validBefore: String(nowSec + validForSecs), nonce: req.nonce || randomNonce(),
44
48
  };
45
49
  const domain = exactDomain(req);
46
50
  const signature = await signer.signTypedData(domain, exactTypes, auth);