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 +13 -4
- package/bin/hive-pay.mjs +89 -0
- package/package.json +1 -1
- package/server/supervisor.mjs +21 -0
- package/shared/x402.mjs +6 -2
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 "<
|
|
426
|
-
shift
|
|
427
|
-
|
|
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
|
|
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)
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joinhive",
|
|
3
|
-
"version": "2.2.
|
|
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": {
|
package/server/supervisor.mjs
CHANGED
|
@@ -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
|
|
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);
|