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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hive-claim-invite — redeem a Buzz community invite with THIS endpoint's key.
|
|
3
|
+
//
|
|
4
|
+
// The invite CODE is minted by a community owner/admin (in the Buzz desktop
|
|
5
|
+
// app). Claiming is NIP-98-signed by the joining key and is exempt from the
|
|
6
|
+
// membership gate by design — so this script only ever uses OUR own agent key.
|
|
7
|
+
// It never touches anyone else's key.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// hive claim-invite <code|invite-url> [--age-confirmed]
|
|
11
|
+
// The code looks like "v2.xxxxx"; a full https://<host>/invite/v2.xxxxx URL
|
|
12
|
+
// is also accepted (the code is parsed out).
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { finalizeEvent } from 'nostr-tools/pure';
|
|
18
|
+
|
|
19
|
+
const sha256hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
20
|
+
|
|
21
|
+
const HIVE_HOME = process.env.HIVE_HOME || join(homedir(), '.hive');
|
|
22
|
+
const identity = JSON.parse(readFileSync(join(HIVE_HOME, 'identity.json'), 'utf8'));
|
|
23
|
+
const cfg = (() => { try { return JSON.parse(readFileSync(join(HIVE_HOME, 'config.json'), 'utf8')); } catch { return {}; } })();
|
|
24
|
+
const RELAY = (process.env.BUZZ_RELAY_URL || cfg.relay || 'http://localhost:3000').replace(/\/$/, '');
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const ageConfirmed = args.includes('--age-confirmed');
|
|
28
|
+
let raw = args.find((a) => !a.startsWith('--'));
|
|
29
|
+
if (!raw) { console.error(JSON.stringify({ error: 'usage: hive claim-invite <code|url> [--age-confirmed]' })); process.exit(1); }
|
|
30
|
+
// Accept a full invite URL or a bare code.
|
|
31
|
+
const code = raw.includes('/invite/') ? raw.split('/invite/')[1].trim() : raw.trim();
|
|
32
|
+
|
|
33
|
+
const sk = Uint8Array.from(Buffer.from(identity.privkey, 'hex'));
|
|
34
|
+
|
|
35
|
+
// Build a NIP-98 Authorization header for one request.
|
|
36
|
+
const nip98 = (method, url, bodyBytes) => {
|
|
37
|
+
const tags = [
|
|
38
|
+
['u', url],
|
|
39
|
+
['method', method],
|
|
40
|
+
['nonce', globalThis.crypto.randomUUID()],
|
|
41
|
+
];
|
|
42
|
+
if (bodyBytes && bodyBytes.length) tags.push(['payload', sha256hex(bodyBytes)]);
|
|
43
|
+
const evt = finalizeEvent({ kind: 27235, created_at: Math.floor(Date.now() / 1000), tags, content: '' }, sk);
|
|
44
|
+
return 'Nostr ' + Buffer.from(JSON.stringify(evt)).toString('base64');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const post = async (path, obj) => {
|
|
48
|
+
const url = `${RELAY}${path}`;
|
|
49
|
+
const body = Buffer.from(JSON.stringify(obj));
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'content-type': 'application/json', authorization: nip98('POST', url, body) },
|
|
53
|
+
body,
|
|
54
|
+
});
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
let json; try { json = JSON.parse(text); } catch { json = { raw: text }; }
|
|
57
|
+
return { status: res.status, json };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const main = async () => {
|
|
61
|
+
// 1) Join policy (terms/age). If present, we must post an acceptance receipt.
|
|
62
|
+
const polRes = await fetch(`${RELAY}/api/join-policy`);
|
|
63
|
+
const policy = (await polRes.json()).policy;
|
|
64
|
+
|
|
65
|
+
let policy_receipt;
|
|
66
|
+
if (policy) {
|
|
67
|
+
if (policy.age_attestation_required && !ageConfirmed) {
|
|
68
|
+
console.error(JSON.stringify({
|
|
69
|
+
error: 'age_attestation_required',
|
|
70
|
+
message: 'This community requires an age attestation. Re-run with --age-confirmed only if you truly meet the age requirement in the terms.',
|
|
71
|
+
version: policy.version,
|
|
72
|
+
}));
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
const acc = await post('/api/invites/accept-policy', {
|
|
76
|
+
code, policy_version: policy.version, age_confirmed: ageConfirmed,
|
|
77
|
+
});
|
|
78
|
+
if (acc.status !== 200) { console.error(JSON.stringify({ step: 'accept-policy', ...acc })); process.exit(1); }
|
|
79
|
+
policy_receipt = acc.json.receipt || acc.json.policy_receipt || acc.json.token;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2) Claim.
|
|
83
|
+
const claim = await post('/api/invites/claim', policy_receipt ? { code, policy_receipt } : { code });
|
|
84
|
+
if (claim.status !== 200) { console.error(JSON.stringify({ step: 'claim', ...claim })); process.exit(1); }
|
|
85
|
+
console.log(JSON.stringify({ joined: true, relay: RELAY, pubkey: identity.pubkey, ...claim.json }));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
main().catch((e) => { console.error(JSON.stringify({ error: String(e && e.message || e) })); process.exit(1); });
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hive-join — full member onboarding, from community link to live bee.
|
|
3
|
+
//
|
|
4
|
+
// hive join --invite <code> --server https://api.<domain> [--export chat.zip] [--name sid]
|
|
5
|
+
//
|
|
6
|
+
// What happens where (the privacy contract):
|
|
7
|
+
// LAPTOP: identity keys, the shared wallet mnemonic (Apple Keychain), your
|
|
8
|
+
// LLM API key (login Keychain, for the sync watcher), and the
|
|
9
|
+
// profile distillation — RAW CHAT HISTORY NEVER LEAVES.
|
|
10
|
+
// UPLOADED: the distilled profile + a nacl.box-sealed {wallet_mnemonic,
|
|
11
|
+
// llm_api_key} that only the bee-host can open (your bee needs the
|
|
12
|
+
// key to think and the wallet to transact 24/7 — your decision #9).
|
|
13
|
+
// Every step is idempotent: re-running resumes wherever it stopped.
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
16
|
+
import { homedir, platform, userInfo } from 'node:os';
|
|
17
|
+
import { join, dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { createInterface } from 'node:readline/promises';
|
|
20
|
+
import nacl from 'tweetnacl';
|
|
21
|
+
import { finalizeEvent } from 'nostr-tools/pure';
|
|
22
|
+
import { signedFetch } from '../shared/nip98.mjs';
|
|
23
|
+
import { computeAuthTag } from '../shared/nip-oa.mjs';
|
|
24
|
+
import { createEngine } from '../daemon/engines/index.mjs';
|
|
25
|
+
import { OPENAI_COMPAT_BASES, MODEL_TIERS } from '../shared/config-schema.mjs';
|
|
26
|
+
|
|
27
|
+
const PACK_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
28
|
+
const HIVE_HOME = process.env.HIVE_HOME || join(homedir(), '.hive');
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
const flag = (n) => { const i = args.indexOf(`--${n}`); return i >= 0 ? args[i + 1] : null; };
|
|
31
|
+
const say = (...a) => console.log('🐝', ...a);
|
|
32
|
+
const die = (msg) => { console.error('❌', msg); process.exit(1); };
|
|
33
|
+
const loadJson = (p, fb) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return fb; } };
|
|
34
|
+
const node = (script, argv = [], opts = {}) => {
|
|
35
|
+
const r = spawnSync(process.execPath, [join(PACK_DIR, script), ...argv], { encoding: 'utf8', env: { ...process.env, HIVE_HOME }, ...opts });
|
|
36
|
+
return { code: r.status, out: (r.stdout || '') + (r.status !== 0 ? (r.stderr || '') : '') };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const INVITE = flag('invite');
|
|
40
|
+
const SERVER = (flag('server') || '').replace(/\/+$/, '');
|
|
41
|
+
if (!INVITE || !SERVER) die('usage: hive join --invite <code> --server https://api.<domain> [--export <claude-export.zip>] [--name <you>]');
|
|
42
|
+
|
|
43
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
+
|
|
45
|
+
const main = async () => {
|
|
46
|
+
// 0. Server hello: provisioning box key + the community's relay URL.
|
|
47
|
+
say(`checking in with ${SERVER} …`);
|
|
48
|
+
let hello;
|
|
49
|
+
try { hello = await (await fetch(`${SERVER}/api/provision-key`, { signal: AbortSignal.timeout(8000) })).json(); }
|
|
50
|
+
catch { die(`can't reach ${SERVER} — check the URL from your invite link`); }
|
|
51
|
+
if (!hello.box_pub || !hello.relay) die('server did not return a provisioning key');
|
|
52
|
+
const RELAY = hello.relay;
|
|
53
|
+
|
|
54
|
+
// 1. Identity (idempotent).
|
|
55
|
+
const kg = node('bin/hive-keygen.mjs');
|
|
56
|
+
if (kg.code !== 0) die(`keygen failed: ${kg.out.slice(0, 200)}`);
|
|
57
|
+
const identity = loadJson(join(HIVE_HOME, 'identity.json'), null) || die('identity missing after keygen');
|
|
58
|
+
say(`your key: ${identity.pubkey.slice(0, 12)}…`);
|
|
59
|
+
|
|
60
|
+
// 2. Join the community relay (config + invite claim).
|
|
61
|
+
const cfgPath = join(HIVE_HOME, 'config.json');
|
|
62
|
+
const cfg = loadJson(cfgPath, {});
|
|
63
|
+
cfg.relay = RELAY;
|
|
64
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
65
|
+
const claim = node('bin/hive-claim-invite.mjs', [INVITE], { env: { ...process.env, HIVE_HOME, BUZZ_RELAY_URL: RELAY } });
|
|
66
|
+
const claimJson = (() => { try { return JSON.parse(claim.out.trim().split('\n').pop()); } catch { return {}; } })();
|
|
67
|
+
if (claim.code !== 0 && !/already/.test(JSON.stringify(claimJson))) {
|
|
68
|
+
if (claimJson.error === 'age_attestation_required') die('this community requires an age attestation — re-run with the CLI flag --age-confirmed via: hive claim-invite');
|
|
69
|
+
// Open relay (dev/local) has no membership gate: if reads work without a
|
|
70
|
+
// claim, continue. On a closed relay the probe 403s and this is fatal.
|
|
71
|
+
const { signedFetch: probeFetch } = await import('../shared/nip98.mjs');
|
|
72
|
+
const probe = await probeFetch(identity.privkey, 'POST', `${RELAY.replace(/^ws/, 'http')}/query`, [{ kinds: [39000], limit: 1 }]);
|
|
73
|
+
if (probe.status !== 200) die(`relay invite claim failed: ${claim.out.slice(0, 300)}`);
|
|
74
|
+
say('relay has no membership gate (open/dev) — continuing without a claim');
|
|
75
|
+
}
|
|
76
|
+
say(`joined relay ${RELAY}`);
|
|
77
|
+
|
|
78
|
+
// 3. Shared wallet — mnemonic into the Apple Keychain, shown ONCE.
|
|
79
|
+
const w = node('bin/hive-wallet.mjs');
|
|
80
|
+
if (w.code !== 0) die(`wallet failed: ${w.out.slice(0, 200)}`);
|
|
81
|
+
const wallet = JSON.parse(w.out.trim().split('\n').pop());
|
|
82
|
+
say(`shared wallet: ${wallet.evm_address}`);
|
|
83
|
+
if (wallet.status === 'created') {
|
|
84
|
+
const exp = node('bin/hive-wallet.mjs', ['export']);
|
|
85
|
+
try {
|
|
86
|
+
const mn = JSON.parse(exp.out).mnemonic;
|
|
87
|
+
say('———— WRITE THIS DOWN (recovery phrase — shown once, stored in your Apple Keychain) ————');
|
|
88
|
+
console.log(` ${mn}`);
|
|
89
|
+
say('————————————————————————————————————————————————————————————————————————');
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 4. Your LLM provider + API key (your bee thinks with YOUR key).
|
|
94
|
+
const name = (flag('name') || cfg.owner_name || userInfo().username || 'member').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 24);
|
|
95
|
+
let provider = flag('provider') || (['anthropic', 'openai', 'openrouter', 'hermes'].includes(cfg.provider) ? cfg.provider : null);
|
|
96
|
+
if (!provider) {
|
|
97
|
+
provider = (await rl.question('LLM provider for your bee [anthropic/openai/openrouter/hermes] (default anthropic): ')).trim().toLowerCase() || 'anthropic';
|
|
98
|
+
}
|
|
99
|
+
if (!['anthropic', 'openai', 'openrouter', 'hermes', 'echo'].includes(provider)) die(`unknown provider "${provider}"`);
|
|
100
|
+
let llmKey = process.env.HIVE_LLM_KEY || null;
|
|
101
|
+
let models = { extract: flag('model-extract'), compute: flag('model-compute') };
|
|
102
|
+
if (provider === 'echo') {
|
|
103
|
+
llmKey = 'echo'; // test seam: no key, no thinking, no spend — rehearsals/CI
|
|
104
|
+
} else {
|
|
105
|
+
if (!llmKey && platform() === 'darwin') {
|
|
106
|
+
try { llmKey = execFileSync('security', ['find-generic-password', '-a', provider, '-s', 'hive-llm-key', '-w'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); say(`using the ${provider} key already in your Keychain`); } catch {}
|
|
107
|
+
}
|
|
108
|
+
if (!llmKey) llmKey = (await rl.question(`${provider} API key (stored in your login Keychain, uploaded sealed for your bee): `)).trim();
|
|
109
|
+
if (!llmKey) die('an API key is required — your bee cannot think without one');
|
|
110
|
+
if ((provider === 'openrouter' || provider === 'hermes') && (!models.extract || !models.compute)) {
|
|
111
|
+
models.extract = models.extract || (await rl.question(`${provider} model id for cheap extraction: `)).trim();
|
|
112
|
+
models.compute = models.compute || (await rl.question(`${provider} model id for strong compute: `)).trim();
|
|
113
|
+
}
|
|
114
|
+
say('validating the key with a 1-token self-test …');
|
|
115
|
+
const tiers = MODEL_TIERS[provider] || {};
|
|
116
|
+
const engine = createEngine({ provider, base_url: OPENAI_COMPAT_BASES[provider], model_extract: models.extract || tiers.extract, model_compute: models.compute || tiers.compute }, { llm_api_key: llmKey }, {});
|
|
117
|
+
const probe = await engine.selftest();
|
|
118
|
+
if (String(probe).startsWith('engine-error')) die(`key check failed: ${probe.slice(0, 200)}`);
|
|
119
|
+
say('key works ✓');
|
|
120
|
+
if (platform() === 'darwin') {
|
|
121
|
+
try { execFileSync('security', ['add-generic-password', '-U', '-a', provider, '-s', 'hive-llm-key', '-l', `Hive LLM key (${provider})`, '-w', llmKey], { stdio: 'ignore' }); } catch {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 5. Distill the private profile LOCALLY (the only data that gets uploaded).
|
|
126
|
+
// An existing profile is PRESERVED (migrations, re-joins) — pass
|
|
127
|
+
// --redistill to rebuild it from scratch.
|
|
128
|
+
mkdirSync(join(HIVE_HOME, 'data-store'), { recursive: true });
|
|
129
|
+
const profilePath = join(HIVE_HOME, 'data-store', 'profile.md');
|
|
130
|
+
if (existsSync(profilePath) && readFileSync(profilePath, 'utf8').trim().length > 50 && !args.includes('--redistill')) {
|
|
131
|
+
say('using your existing profile (pass --redistill to rebuild it)');
|
|
132
|
+
} else {
|
|
133
|
+
say('distilling your profile from local chat history (raw chats never leave this machine) …');
|
|
134
|
+
const distillArgs = ['--provider', provider, '--key', llmKey, '--out', profilePath];
|
|
135
|
+
if (flag('export')) distillArgs.push('--export', flag('export'));
|
|
136
|
+
if (models.extract) distillArgs.push('--model', models.extract);
|
|
137
|
+
const d = node('watcher/distill.mjs', distillArgs);
|
|
138
|
+
if (d.code !== 0 || !existsSync(profilePath)) die(`distillation failed: ${d.out.slice(0, 300)}`);
|
|
139
|
+
}
|
|
140
|
+
const profile = readFileSync(profilePath, 'utf8');
|
|
141
|
+
say(`profile ready (${profile.length} chars) — review anytime: ~/.hive/data-store/profile.md`);
|
|
142
|
+
|
|
143
|
+
// 6. Seal {mnemonic, llm key} to the server's box key and provision the bee.
|
|
144
|
+
const mnemonic = (() => {
|
|
145
|
+
try { return JSON.parse(node('bin/hive-wallet.mjs', ['export']).out).mnemonic; } catch { return null; }
|
|
146
|
+
})();
|
|
147
|
+
if (!mnemonic) die('could not read the wallet mnemonic back from the Keychain');
|
|
148
|
+
const client = nacl.box.keyPair();
|
|
149
|
+
const nonce = nacl.randomBytes(24);
|
|
150
|
+
const box = nacl.box(
|
|
151
|
+
Buffer.from(JSON.stringify({ wallet_mnemonic: mnemonic, llm_api_key: llmKey })),
|
|
152
|
+
nonce, Buffer.from(hello.box_pub, 'base64'), client.secretKey,
|
|
153
|
+
);
|
|
154
|
+
say(`provisioning ${name}.bee on the community server …`);
|
|
155
|
+
const body = {
|
|
156
|
+
invite: INVITE, name, owner_pubkey: identity.pubkey, owner_name: name,
|
|
157
|
+
evm_address: wallet.evm_address, provider,
|
|
158
|
+
...(OPENAI_COMPAT_BASES[provider] ? { base_url: OPENAI_COMPAT_BASES[provider] } : {}),
|
|
159
|
+
...(models.extract ? { model_extract: models.extract } : {}),
|
|
160
|
+
...(models.compute ? { model_compute: models.compute } : {}),
|
|
161
|
+
sealed: { nonce: Buffer.from(nonce).toString('base64'), box: Buffer.from(box).toString('base64'), client_pub: Buffer.from(client.publicKey).toString('base64') },
|
|
162
|
+
profile_md: profile,
|
|
163
|
+
};
|
|
164
|
+
const resp = await signedFetch(identity.privkey, 'POST', `${SERVER}/api/bees`, body);
|
|
165
|
+
if (resp.status !== 200) die(`provisioning failed (http ${resp.status}): ${JSON.stringify(resp.json).slice(0, 300)}`);
|
|
166
|
+
const beePubkey = resp.json.bee_pubkey;
|
|
167
|
+
say(`${name}.bee exists: ${String(beePubkey).slice(0, 12)}…`);
|
|
168
|
+
|
|
169
|
+
// 6b. Buzz Agents directory (NIP-OA): the OWNER key signs both halves of
|
|
170
|
+
// the pairing right here — it never leaves this laptop.
|
|
171
|
+
// (a) kind-30177 managed-agent record (owner-authored, bee pubkey in
|
|
172
|
+
// the d tag), published straight to the relay: the relay requires
|
|
173
|
+
// event author == HTTP signer, so only this laptop can publish it.
|
|
174
|
+
// (b) the NIP-OA auth tag co-signature over the bee's pubkey, shipped
|
|
175
|
+
// to the server (it holds the bee key) so the bee's kind-0 profile
|
|
176
|
+
// carries it. Together these make the bee show up under Agents in
|
|
177
|
+
// the Buzz desktop app, cryptographically verified as YOURS.
|
|
178
|
+
// Best-effort: the bee works without it; a re-join retries both halves.
|
|
179
|
+
if (/^[0-9a-f]{64}$/.test(String(beePubkey))) {
|
|
180
|
+
try {
|
|
181
|
+
const httpRelay = RELAY.replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://').replace(/\/+$/, '');
|
|
182
|
+
const ownerSk = Uint8Array.from(Buffer.from(identity.privkey, 'hex'));
|
|
183
|
+
const record = finalizeEvent({
|
|
184
|
+
kind: 30177, created_at: Math.floor(Date.now() / 1000),
|
|
185
|
+
tags: [['d', beePubkey]],
|
|
186
|
+
content: JSON.stringify({ name: `${name}.bee`, parallelism: 1, respond_to: 'anyone' }),
|
|
187
|
+
}, ownerSk);
|
|
188
|
+
const pub = await signedFetch(identity.privkey, 'POST', `${httpRelay}/events`, record);
|
|
189
|
+
if (pub.status !== 200 || pub.json?.accepted !== true) throw new Error(`kind-30177 publish: http=${pub.status} ${JSON.stringify(pub.json).slice(0, 160)}`);
|
|
190
|
+
const authTag = computeAuthTag(identity.privkey, beePubkey, '');
|
|
191
|
+
const resp2 = await signedFetch(identity.privkey, 'POST', `${SERVER}/api/bees`, { ...body, nip_oa_auth: authTag });
|
|
192
|
+
if (resp2.status !== 200) throw new Error(`auth-tag delivery: http=${resp2.status} ${JSON.stringify(resp2.json).slice(0, 160)}`);
|
|
193
|
+
say(`registered in the Buzz Agents directory (owner-verified: ${name}.bee)`);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
say(`⚠️ Buzz Agents registration incomplete (${String(e.message).slice(0, 160)}) — re-run this join to retry; the bee itself is unaffected`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 7. Remember the pairing locally + enable sync, and publish YOUR kind-0
|
|
200
|
+
// profile so apps (Buzz desktop included) show your name, not a pubkey.
|
|
201
|
+
const cfg2 = loadJson(cfgPath, {});
|
|
202
|
+
Object.assign(cfg2, { owner_name: name, bee_name: `${name}.bee`, bee_pubkey: beePubkey, server_url: SERVER, sync: { enabled: true, provider } });
|
|
203
|
+
writeFileSync(cfgPath, JSON.stringify(cfg2, null, 2) + '\n');
|
|
204
|
+
const prof = node('bin/hive-net.mjs', ['set-name', name]);
|
|
205
|
+
if (prof.code === 0) say(`your profile name is set: ${name}`);
|
|
206
|
+
|
|
207
|
+
// 8. Auto-sync watcher (launchd, macOS). --no-watcher skips it — used for
|
|
208
|
+
// secondary endpoints on the same machine (one watcher per Mac).
|
|
209
|
+
if (platform() === 'darwin' && !args.includes('--no-watcher')) {
|
|
210
|
+
try {
|
|
211
|
+
const tmpl = readFileSync(join(PACK_DIR, 'watcher', 'global.nfh.hive.sync.plist.tmpl'), 'utf8')
|
|
212
|
+
.replaceAll('__NODE__', process.execPath).replaceAll('__PACK__', PACK_DIR).replaceAll('__HOME__', homedir());
|
|
213
|
+
const plist = join(homedir(), 'Library', 'LaunchAgents', 'global.nfh.hive.sync.plist');
|
|
214
|
+
mkdirSync(dirname(plist), { recursive: true });
|
|
215
|
+
writeFileSync(plist, tmpl);
|
|
216
|
+
execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' });
|
|
217
|
+
try { execFileSync('launchctl', ['load', plist], { stdio: 'ignore' }); } catch {}
|
|
218
|
+
say('auto-sync watcher installed (every 15 min; hive sync off to stop)');
|
|
219
|
+
} catch (e) { say(`watcher install skipped (${String(e.message).slice(0, 80)}) — run: hive sync now`); }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 9. Wait for the bee to come alive + grants to land.
|
|
223
|
+
say('waiting for your bee to wake up (engine self-test + genesis grants) …');
|
|
224
|
+
for (let i = 0; i < 30; i++) {
|
|
225
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
226
|
+
try {
|
|
227
|
+
const st = await (await fetch(`${SERVER}/api/bees/${name}/status`, { signal: AbortSignal.timeout(5000) })).json();
|
|
228
|
+
if (st.status === 'ready') {
|
|
229
|
+
say('———— YOU ARE IN ————');
|
|
230
|
+
say(`bee: ${name}.bee (${String(beePubkey).slice(0, 12)}…)`);
|
|
231
|
+
say(`wallet: ${wallet.evm_address} (shared: you + your bee)`);
|
|
232
|
+
if (st.grants?.jelly_tx) say(`grant: 500 JELLY — https://sepolia.etherscan.io/tx/${st.grants.jelly_tx}`);
|
|
233
|
+
say(`try: hive ask "what should we watch this weekend" · hive feed · hive leaderboard`);
|
|
234
|
+
rl.close();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
say('provisioned, but the bee has not heartbeated yet — check later: hive agent status');
|
|
240
|
+
rl.close();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
main().catch((e) => { rl.close(); die(String(e.message || e)); });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generate (or print) the Hive endpoint identity at ~/.hive/identity.json.
|
|
3
|
+
// Idempotent: an existing identity is never overwritten (join once — the same
|
|
4
|
+
// endpoint is reused by every client: Claude Code, Codex, Hermes, ...).
|
|
5
|
+
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
6
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
const dir = process.env.HIVE_HOME || join(homedir(), '.hive'); // respect HIVE_HOME like wallet/CLI
|
|
11
|
+
const file = join(dir, 'identity.json');
|
|
12
|
+
|
|
13
|
+
if (existsSync(file)) {
|
|
14
|
+
const id = JSON.parse(readFileSync(file, 'utf8'));
|
|
15
|
+
// Migration: v1 identities predate the owner keypair (used for private
|
|
16
|
+
// relay memory — NIP-AE engrams need a distinct owner key). Add one once.
|
|
17
|
+
if (!id.owner_pubkey) {
|
|
18
|
+
const osk = generateSecretKey();
|
|
19
|
+
id.owner_privkey = Buffer.from(osk).toString('hex');
|
|
20
|
+
id.owner_pubkey = getPublicKey(osk);
|
|
21
|
+
id.version = 2;
|
|
22
|
+
writeFileSync(file, JSON.stringify(id, null, 2) + '\n', { mode: 0o600 });
|
|
23
|
+
}
|
|
24
|
+
console.log(JSON.stringify({ status: 'existing', pubkey: id.pubkey, npub: id.npub, owner_pubkey: id.owner_pubkey, created_at: id.created_at }));
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sk = generateSecretKey();
|
|
29
|
+
const skHex = Buffer.from(sk).toString('hex');
|
|
30
|
+
const pk = getPublicKey(sk);
|
|
31
|
+
const osk = generateSecretKey(); // owner key: recovery + private-memory owner
|
|
32
|
+
const identity = {
|
|
33
|
+
version: 2,
|
|
34
|
+
privkey: skHex,
|
|
35
|
+
nsec: nip19.nsecEncode(sk),
|
|
36
|
+
pubkey: pk,
|
|
37
|
+
npub: nip19.npubEncode(pk),
|
|
38
|
+
owner_privkey: Buffer.from(osk).toString('hex'),
|
|
39
|
+
owner_pubkey: getPublicKey(osk),
|
|
40
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
41
|
+
};
|
|
42
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
43
|
+
for (const sub of ['data-store', 'capability-store', 'object-store']) {
|
|
44
|
+
mkdirSync(join(dir, sub), { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
writeFileSync(file, JSON.stringify(identity, null, 2) + '\n', { mode: 0o600 });
|
|
47
|
+
chmodSync(file, 0o600);
|
|
48
|
+
console.log(JSON.stringify({ status: 'created', pubkey: pk, npub: identity.npub, created_at: identity.created_at }));
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Mint a playful Hive object through computational proof-of-work.
|
|
3
|
+
// The object id is sha256 over the canonical JSON (sorted keys, no id field);
|
|
4
|
+
// a mint is valid when the id has >= `difficulty` leading zero bits. Anyone
|
|
5
|
+
// can verify a mint by recomputing one hash — scarcity comes from the work.
|
|
6
|
+
//
|
|
7
|
+
// Usage: hive-mint.mjs --difficulty 16 [--name "..."] [--emoji "🐝"]
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
const args = Object.fromEntries(
|
|
14
|
+
process.argv.slice(2).map((a, i, arr) => (a.startsWith('--') ? [a.slice(2), arr[i + 1]] : null)).filter(Boolean),
|
|
15
|
+
);
|
|
16
|
+
const difficulty = Number(args.difficulty ?? 16);
|
|
17
|
+
if (!Number.isInteger(difficulty) || difficulty < 8 || difficulty > 28) {
|
|
18
|
+
console.error(JSON.stringify({ error: 'difficulty must be an integer in 8..28' }));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const identity = JSON.parse(readFileSync(join(process.env.HIVE_HOME || join(homedir(), '.hive'), 'identity.json'), 'utf8'));
|
|
23
|
+
|
|
24
|
+
const ADJ = ['Amber', 'Waggle', 'Golden', 'Drowsy', 'Zippy', 'Velvet', 'Honeyed', 'Fuzzy', 'Gilded', 'Nectar', 'Dappled', 'Humming'];
|
|
25
|
+
const NOUN = ['Hexagon', 'Drone', 'Comb', 'Pollen Puff', 'Stinger', 'Queen Cell', 'Waggle Dance', 'Propolis Chip', 'Royal Jelly', 'Swarm Stone', 'Bee Bread', 'Wax Sigil'];
|
|
26
|
+
const EMOJI = ['🐝', '🍯', '🌻', '🧡', '🔶', '👑', '💛', '🌼'];
|
|
27
|
+
const pick = (list, seed) => list[((seed % list.length) + list.length) % list.length];
|
|
28
|
+
const seed = parseInt(identity.pubkey.slice(0, 8), 16) + Date.now();
|
|
29
|
+
|
|
30
|
+
const RARITY = difficulty >= 24 ? 'legendary' : difficulty >= 20 ? 'rare' : difficulty >= 16 ? 'uncommon' : 'common';
|
|
31
|
+
const base = {
|
|
32
|
+
v: 1,
|
|
33
|
+
kind: 'hive-object',
|
|
34
|
+
name: args.name ?? `${pick(ADJ, seed)} ${pick(NOUN, Math.floor(seed / 16))}`,
|
|
35
|
+
emoji: args.emoji ?? pick(EMOJI, Math.floor(seed / 4)),
|
|
36
|
+
rarity: RARITY,
|
|
37
|
+
difficulty,
|
|
38
|
+
owner: identity.pubkey,
|
|
39
|
+
minted_at: Math.floor(Date.now() / 1000),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const canonical = (obj) => JSON.stringify(Object.fromEntries(Object.entries(obj).sort(([a], [b]) => (a < b ? -1 : 1))));
|
|
43
|
+
const leadingZeroBits = (buf) => {
|
|
44
|
+
let bits = 0;
|
|
45
|
+
for (const byte of buf) {
|
|
46
|
+
if (byte === 0) { bits += 8; continue; }
|
|
47
|
+
bits += Math.clz32(byte) - 24;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return bits;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const t0 = Date.now();
|
|
54
|
+
let nonce = 0, id;
|
|
55
|
+
for (;;) {
|
|
56
|
+
const digest = createHash('sha256').update(canonical({ ...base, nonce })).digest();
|
|
57
|
+
if (leadingZeroBits(digest) >= difficulty) { id = digest.toString('hex'); break; }
|
|
58
|
+
nonce++;
|
|
59
|
+
}
|
|
60
|
+
const object = { ...base, nonce, id };
|
|
61
|
+
const outDir = join(process.env.HIVE_HOME || join(homedir(), '.hive'), 'object-store');
|
|
62
|
+
mkdirSync(outDir, { recursive: true });
|
|
63
|
+
const outFile = join(outDir, `${id.slice(0, 16)}.json`);
|
|
64
|
+
writeFileSync(outFile, JSON.stringify(object, null, 2) + '\n');
|
|
65
|
+
console.log(JSON.stringify({ ...object, hashes: nonce + 1, ms: Date.now() - t0, file: outFile }));
|